# Documentation complète : Debian vierge → SSH proxy transparent ## Vue d'ensemble de l'architecture ``` [Windows Client] ↓ (clé lab_rsa) [Gateway sshproxy] ← SSH sur port 2222 ↓ (clé gateway_rsa, transparent) [Destination 1 ou 2] ``` --- ## NIVEAU 1: Infrastructure Docker (docker-compose.yaml) ### Rôle Définit 3 conteneurs Debian isolés dans un réseau privé avec IPs fixes. ```yaml 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**: - **Port 2222**: Accès SSH depuis Windows via `ssh -p 2222 localhost` - **IPs statiques**: Permet à sshproxy de cibler dest1/dest2 de manière prévisible - **Réseau privé**: Conteneurs isolés, ne voient que les autres conteneurs du réseau --- ## NIVEAU 2: Gateway — Dockerfile multi-stage Le Dockerfile gateway a **2 stages**: ### Stage 1: Builder (compilation Go) ```dockerfile 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?** - Stage 1 (builder): Contient Go compiler (1 GB+) — lourd - Stage 2 (final): Copie que les binaires compilés — léger ### Stage 2: Image finale ```dockerfile 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) ``` --- ## NIVEAU 3: Gateway — Configuration SSH (sshd_config) ```bash 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 ``` --- ## NIVEAU 4: Gateway — Wrapper sshproxy (sshproxy-wrapper.sh) ```bash #!/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?** - Permet de faire du preprocessing avant sshproxy si nécessaire - Future évolution: ajouter de l'audit, des ACLs, etc. --- ## NIVEAU 5: Gateway — Configuration sshproxy (sshproxy.yaml) ```yaml --- 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**: 1. Client Windows: `ssh -p 2222 testuser@localhost 'hostname'` 2. sshd gateway accepte la connexion (clé lab_rsa validée) 3. ForceCommand lance sshproxy-wrapper 4. Wrapper lance `/usr/sbin/sshproxy` avec la commande 'hostname' 5. sshproxy lit sshproxy.yaml 6. Choisit random: dest1 ou dest2 7. Exécute: `ssh -tt -i /etc/sshproxy/gateway_rsa testuser@172.30.0.12 hostname` 8. dest2 accepte (clé gateway_rsa.pub reconnue) 9. Résultat 'dest2' retourné à Windows --- ## NIVEAU 6: Destinations — Dockerfile ```dockerfile FROM 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"] ``` --- ## NIVEAU 7: Destinations — Configuration SSH (dest/sshd_config) ```bash 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**: - **Gateway**: ForceCommand /usr/sbin/sshproxy-wrapper (intercepte tout) - **Destinations**: Aucun ForceCommand (SSH normal) --- ## NIVEAU 8: Gestion des clés SSH Trois clés SSH en jeu: ### 1. lab_rsa (Windows → Gateway) ``` Client Windows Gateway [lab_rsa.pub] ←→ [authorized_keys] clé privée clé publique ssh -i lab_rsa testuser@gateway ``` ### 2. gateway_rsa (Gateway → Destinations) ``` 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 ``` ### 3. Clés d'identité daemon SSH (host keys) ``` 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_* ``` --- ## FLUX COMPLET D'UNE CONNEXION ``` 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 ``` --- ## RÉSUMÉ: Points critiques pour la transparence | 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 | --- ## Commandes utiles ```bash # 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 ```