05_DETAILS_TECHNIQUES_COMPLETS.md 17 KB

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.

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)

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

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)

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)

#!/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)

---
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

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)

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

# 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