[Windows Client]
↓ (clé lab_rsa)
[Gateway sshproxy] ← SSH sur port 2222
↓ (clé gateway_rsa, transparent)
[Destination 1 ou 2]
Définit 3 conteneurs Debian isolés dans un réseau privé avec IPs fixes.
services:
gateway: # Conteneur gateway (le proxy)
build:
context: .
dockerfile: gateway/Dockerfile
container_name: sshproxy-gateway
hostname: gateway # Nom du conteneur
ports:
- "2222:22" # Expose SSH sur port 2222 de l'hôte
networks:
sshproxy_net:
ipv4_address: 172.30.0.10 # IP fixe interne au réseau Docker
restart: unless-stopped
dest1: # Conteneur destination 1
build:
context: .
dockerfile: dest/Dockerfile
container_name: sshproxy-dest1
hostname: dest1 # Nom du conteneur
networks:
sshproxy_net:
ipv4_address: 172.30.0.11 # IP fixe pour sshproxy
restart: unless-stopped
dest2: # Conteneur destination 2 (même config que dest1)
build:
context: .
dockerfile: dest/Dockerfile
container_name: sshproxy-dest2
hostname: dest2
networks:
sshproxy_net:
ipv4_address: 172.30.0.12
restart: unless-stopped
networks:
sshproxy_net: # Réseau bridge isolé
name: sshproxy_net
driver: bridge # Type de réseau Docker
ipam:
config:
- subnet: 172.30.0.0/24 # Plage IP privée
Clés concepts:
ssh -p 2222 localhostLe Dockerfile gateway a 2 stages:
FROM golang:1.24-bookworm AS builder # Image Go complète (compilateur + dépendances)
ARG SSHPROXY_VERSION=2.1.0 # Version de sshproxy à compiler
RUN apt-get update && apt-get install -y --no-install-recommends \
git ca-certificates make && rm -rf /var/lib/apt/lists/*
# ↑ Dépendances minimales pour cloner + compiler Go
WORKDIR /build # Répertoire de travail
RUN git clone --depth 1 --branch v${SSHPROXY_VERSION} \
https://github.com/cea-hpc/sshproxy.git .
# ↑ Clone sshproxy depuis GitHub (version 2.1.0)
# --depth 1 = clone léger (historique court)
RUN go build -mod=vendor -ldflags "-X main.SshproxyVersion=${SSHPROXY_VERSION}" \
-o bin/sshproxy github.com/cea-hpc/sshproxy/cmd/sshproxy && \
go build -mod=vendor -ldflags "-X main.SshproxyVersion=${SSHPROXY_VERSION}" \
-o bin/sshproxy-dumpd github.com/cea-hpc/sshproxy/cmd/sshproxy-dumpd && \
go build -mod=vendor -ldflags "-X main.SshproxyVersion=${SSHPROXY_VERSION}" \
-o bin/sshproxy-replay github.com/cea-hpc/sshproxy/cmd/sshproxy-replay && \
go build -mod=vendor -ldflags "-X main.SshproxyVersion=${SSHPROXY_VERSION}" \
-o bin/sshproxyctl github.com/cea-hpc/sshproxy/cmd/sshproxyctl
# ↑ Compile 4 binaires sshproxy dans ./bin/
# -mod=vendor = utilise les dépendances vendorisées (dans le repo)
# -ldflags "-X main.SshproxyVersion=..." = injecte la version dans le binaire
Pourquoi 2 stages?
FROM debian:bookworm-slim # Image Debian minimale (~60 MB vs 1.2 GB golang)
RUN apt-get update && apt-get install -y --no-install-recommends \
openssh-server \ # Daemon SSH
ca-certificates && \ # Certificats SSL/TLS
rm -rf /var/lib/apt/lists/* # Nettoie cache apt
# ──── BINAIRES SSHPROXY ────
COPY --from=builder /build/bin/sshproxy /usr/sbin/sshproxy
COPY --from=builder /build/bin/sshproxy-dumpd /usr/sbin/sshproxy-dumpd
COPY --from=builder /build/bin/sshproxyctl /usr/bin/sshproxyctl
COPY --from=builder /build/bin/sshproxy-replay /usr/bin/sshproxy-replay
# ↑ Copie binaires compilés du stage builder vers l'image finale
RUN chmod 755 /usr/sbin/sshproxy /usr/sbin/sshproxy-dumpd \
/usr/bin/sshproxyctl /usr/bin/sshproxy-replay
# ↑ Rend les binaires exécutables
# ──── COMPTE UTILISATEUR ────
RUN useradd -m -s /bin/bash testuser && \
echo "testuser:testuser" | chpasswd && \
mkdir -p /home/testuser/.ssh && \
chmod 700 /home/testuser/.ssh
# ↑ Crée compte testuser
# -m = crée home directory
# -s /bin/bash = shell par défaut
# chmod 700 = seul testuser peut lister son .ssh
# ──── AUTHENTIFICATION GATEWAY→DESTINATIONS ────
# La clé gateway_rsa est utilisée par sshproxy pour se connecter à dest1/dest2
RUN mkdir -p /etc/sshproxy && chmod 755 /etc/sshproxy
# ↑ Crée répertoire pour config sshproxy
# chmod 755 = rend traversable par testuser
COPY keys/gateway_rsa /etc/sshproxy/gateway_rsa
# ↑ Copie clé privée (générée avant le build)
RUN chmod 600 /etc/sshproxy/gateway_rsa && chown testuser:testuser /etc/sshproxy/gateway_rsa
# ↑ chmod 600 = seul le propriétaire peut lire
# chown testuser = testuser est propriétaire (CRUCIAL!)
# → sshproxy s'exécute en tant que testuser, donc elle peut lire la clé
# ──── AUTHENTIFICATION CLIENTS→GATEWAY ────
# Les clients (Windows) utilisent lab_rsa pour se connecter à la gateway
COPY keys/lab_rsa.pub /home/testuser/.ssh/authorized_keys
# ↑ Clé publique du client dans authorized_keys de la gateway
RUN chmod 600 /home/testuser/.ssh/authorized_keys && \
chown -R testuser:testuser /home/testuser/.ssh
# ↑ Permissions SSH standard: seul le propriétaire peut lire
# ──── CONFIGURATION SSHD ────
RUN mkdir -p /run/sshd
# ↑ Répertoire nécessaire au daemon sshd
COPY gateway/sshd_config /etc/ssh/sshd_config
# ↑ Configuration personnalisée SSH daemon
# ──── CONFIGURATION SSHPROXY ────
COPY gateway/sshproxy.yaml /etc/sshproxy/sshproxy.yaml
# ↑ Config du proxy (destinations, logique de routage, etc.)
# ──── WRAPPER SSHPROXY ────
COPY gateway/sshproxy-wrapper.sh /usr/sbin/sshproxy-wrapper
RUN chmod 755 /usr/sbin/sshproxy-wrapper
# ↑ Script shell qui détecte shell interactif vs commande
EXPOSE 22
# ↑ Déclare que le port 22 écoute (docker compose le remaponne sur 2222)
CMD ["/usr/sbin/sshd", "-D", "-e"]
# ↑ Lance sshd en foreground (-D) avec log stderr (-e)
Port 22
ListenAddress 0.0.0.0
# ↑ Écoute SSH sur 0.0.0.0:22
# (remappé sur 127.0.0.1:2222 par docker-compose)
# ──── AUTHENTIFICATION ────
PasswordAuthentication no
# ↑ Désactive auth par mot de passe
# Force utilisation des clés SSH uniquement
PubkeyAuthentication yes
# ↑ Active authentification par clé publique
AuthorizedKeysFile .ssh/authorized_keys
# ↑ Chemin des clés publiques acceptées (relatif au home de l'utilisateur)
# Pour testuser: /home/testuser/.ssh/authorized_keys
PermitRootLogin no
# ↑ Interdit connexion SSH en tant que root (sécurité)
# ──── FORCECOMMAND : LE CŒUR DE LA MAGIE ────
ForceCommand /usr/sbin/sshproxy-wrapper
# ↑ CRUCIAL: Toute connexion SSH exécute d'abord ce wrapper
# Cela intercepte CHAQUE commande SSH et la passe à sshproxy
# C'est ce qui rend le proxy transparent
# ──── SÉCURITÉ RÉSEAU ────
AllowTcpForwarding no
# ↑ Désactive le tunneling SSH (ssh -L / ssh -R)
# Force les utilisateurs à utiliser le proxy directement
X11Forwarding no
# ↑ Désactive X11 forwarding (affichage graphique sur SSH)
# ──── KEEPALIVE ────
ClientAliveInterval 30
ClientAliveCountMax 3
# ↑ Envoie un ping toutes les 30s après 3 sans réponse = déconnexion
# Évite les connexions zombies
LogLevel INFO
# ↑ Niveau de log (DEBUG pour débugging, INFO en production)
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
# ↑ Clés d'identité du daemon SSH (générées auto au premier démarrage)
# Permettent aux clients de vérifier qu'ils parlent au bon serveur
#!/bin/bash
# Ce script s'exécute TOUJOURS en premier pour chaque connexion SSH
# (à cause de ForceCommand dans sshd_config)
if [ -z "$SSH_ORIGINAL_COMMAND" ]; then
# Cas 1: Pas de commande fournie
# = Shell interactif (utilisateur tape ssh gateway sans commande)
exec /usr/sbin/sshproxy
# ↑ Exécute sshproxy en mode shell interactif
# sshproxy va ouvrir un shell sur la destination choisie
# L'utilisateur peut taper des commandes interactivement
else
# Cas 2: Commande fournie
# = Exécution de commande (ssh gateway 'commande')
exec /usr/sbin/sshproxy
# ↑ Exécute sshproxy avec la commande
# sshproxy proxifie la commande vers la destination
# Retour du résultat à l'utilisateur
fi
# Note: Les deux cas font la même chose!
# sshproxy détecte automatiquement si c'est interactif ou commande
# Ce wrapper pourrait être simplifié en:
# exec /usr/sbin/sshproxy
Pourquoi ce wrapper?
---
log: "/tmp/sshproxy-{user}.log"
# ↑ Fichier de log
# {user} = remplacé par le nom d'utilisateur (ex: /tmp/sshproxy-testuser.log)
log_level: "debug"
# ↑ Niveau de verbosité des logs (debug = très détaillé)
# ──── COMMANDE SSH UTILISÉE POUR LE REBOND ────
ssh:
exe: "/usr/bin/ssh"
# ↑ Chemin vers le binaire ssh à utiliser pour les rebonds
args:
- "-v"
# ↑ Verbose: affiche les logs SSH (aide au debugging)
- "-tt"
# ↑ CRUCIAL: Force PTY allocation sur TOUS les rebonds
# -t = demande allocation PTY (terminal pseudo)
# -tt = force même si pas de TTY en entrée
# Permet les shells interactifs sur la destination
- "-i"
# ↑ Spécifie clé privée...
- "/etc/sshproxy/gateway_rsa"
# ↑ ...qui est gateway_rsa
# Utilisée pour auth gateway→dest1/dest2
- "-o"
- "StrictHostKeyChecking=no"
# ↑ Désactive vérification de l'identité du serveur distant
# Évite les "ECDSA key fingerprint... Are you sure?" en non-interactif
- "-o"
- "UserKnownHostsFile=/dev/null"
# ↑ Désactive known_hosts
# Évite les "Warning: Permanently added..." lors du rebond
# ──── CONFIGURATION ETCD (pour sessions persistantes) ────
etcd:
endpoints: []
# ↑ Pas d'endpoints etcd = mode stateless
# Pas de persistance de session
mandatory: false
# ↑ etcd n'est pas obligatoire
# Si pas d'etcd, utiliser le mode stateless par défaut
# ──── DESTINATIONS DU PROXY ────
dest:
- "172.30.0.11:22" # dest1: IP fixe Docker + port SSH
- "172.30.0.12:22" # dest2: IP fixe Docker + port SSH
# ──── STRATÉGIE DE SÉLECTION DES DESTINATIONS ────
route_select: "random"
# ↑ Chaque connexion choisit aléatoirement entre dest1/dest2
# Alternative: "round_robin" = alternance stricte
mode: "balanced"
# ↑ Mode balanced = répartition équilibrée
# Essaie de garder le nombre de sessions équilibré
Flux d'exécution sshproxy:
ssh -p 2222 testuser@localhost 'hostname'/usr/sbin/sshproxy avec la commande 'hostname'ssh -tt -i /etc/sshproxy/gateway_rsa testuser@172.30.0.12 hostnameFROM debian:bookworm-slim
# ↑ Même image de base que la gateway (pour cohérence)
RUN apt-get update && apt-get install -y --no-install-recommends \
openssh-server && \
rm -rf /var/lib/apt/lists/*
# ↑ Installe uniquement sshd
# Pas besoin de sshproxy sur les destinations
# ──── COMPTE UTILISATEUR ────
RUN useradd -m -s /bin/bash testuser && \
echo "testuser:testuser" | chpasswd && \
mkdir -p /home/testuser/.ssh && \
chmod 700 /home/testuser/.ssh
# ↑ Même compte que sur la gateway
# Permet des connexions cohérentes
# ──── AUTHENTIFICATION GATEWAY→DESTINATION ────
COPY keys/gateway_rsa.pub /home/testuser/.ssh/authorized_keys
# ↑ Accepte la clé publique de la gateway
# C'est la clé privée que la gateway utilise pour se connecter
RUN chmod 600 /home/testuser/.ssh/authorized_keys && \
chown -R testuser:testuser /home/testuser/.ssh
# ↑ Permissions standard SSH
# ──── CONFIGURATION SSHD ────
RUN mkdir -p /run/sshd
COPY dest/sshd_config /etc/ssh/sshd_config
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D", "-e"]
Port 22
ListenAddress 0.0.0.0
# ↑ Écoute standard SSH
# ──── AUTHENTIFICATION ────
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PermitRootLogin no
# ↑ Mêmes règles que la gateway
# Authentification par clé uniquement
# ──── DIFFÉRENCE CLÉE ────
# PAS DE ForceCommand ici!
# Les destinations acceptent SSH normalement
# (Pas de proxy intermédiaire)
AllowTcpForwarding no
X11Forwarding no
ClientAliveInterval 30
ClientAliveCountMax 3
LogLevel INFO
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
Différence gateway vs dest:
Trois clés SSH en jeu:
Client Windows Gateway
[lab_rsa.pub] ←→ [authorized_keys]
clé privée clé publique
ssh -i lab_rsa testuser@gateway
Gateway dest1/dest2
[gateway_rsa] ←→ [gateway_rsa.pub]
clé privée dans authorized_keys
sshproxy exécute:
ssh -i /etc/sshproxy/gateway_rsa testuser@dest1
Chaque daemon SSH a sa propre paire de clés RSA/ED25519
Permet aux clients de vérifier l'identité du serveur
Auto-générées au premier démarrage
Chemin: /etc/ssh/ssh_host_*
1. Windows client lance:
ssh -p 2222 -i lab_rsa testuser@localhost 'hostname'
2. Connexion établie à Gateway (172.30.0.10:22 via port 2222)
3. Gateway sshd:
- Vérifie clé: lab_rsa.pub == /home/testuser/.ssh/authorized_keys ✓
- Valide utilisateur: testuser ✓
4. sshd exécute ForceCommand:
- Lance: /usr/sbin/sshproxy-wrapper
5. sshproxy-wrapper détecte:
- SSH_ORIGINAL_COMMAND = "hostname"
- Exécute: /usr/sbin/sshproxy
6. sshproxy lit sshproxy.yaml:
- Destinations: ["172.30.0.11:22", "172.30.0.12:22"]
- Sélection: random
- Choisit: 172.30.0.12 (dest2)
7. sshproxy exécute:
ssh -tt -i /etc/sshproxy/gateway_rsa testuser@172.30.0.12 'hostname'
8. Connection Gateway→dest2 établie:
- Vérifie clé: gateway_rsa (privée de la gateway)
- Accepte via: gateway_rsa.pub (publique sur dest2)
- Valide utilisateur: testuser ✓
9. dest2 sshd exécute:
/bin/bash -c 'hostname'
10. Résultat:
dest2
11. Retour à Windows via Gateway
Affiche: dest2
Exit status: 0
| Point | Pourquoi |
|---|---|
| ForceCommand sshproxy-wrapper | Intercepte chaque SSH et la proxifie |
| chown testuser gateway_rsa | sshproxy (testuser) peut lire la clé privée |
| -tt dans sshproxy.yaml | Alloue PTY pour shells interactifs |
| IPs statiques (172.30.0.x) | sshproxy doit cibler des IPs fixes/prévisibles |
| Pas de ForceCommand sur dest | Les destinations acceptent SSH normal |
| route_select: random | Répartition automatique des connexions |
# Test simple commande
ssh -p 2222 testuser@localhost 'hostname'
# Test shell interactif
ssh -p 2222 testuser@localhost
# Vérifier les logs sshproxy
docker exec sshproxy-gateway cat /tmp/sshproxy-testuser.log
# Tester round-robin (doit altern dest1/dest2)
for i in {1..10}; do ssh -p 2222 testuser@localhost 'hostname'; sleep 0.5; done