Reverse SSH tunnel : atteindre un serveur derrière un NAT

Reverse SSH tunnel : atteindre un serveur derrière un NAT

Accédez à un serveur, un Raspberry Pi ou un poste client qui est derrière un NAT, un firewall, ou une box résidentielle, sans configurer de port forwarding. Le serveur initie la connexion vers un VPS bastion, vous vous connectez via le bastion.

Introduction

Vous avez un serveur, une machine cliente, un Raspberry Pi, ou un NAS chez un client / chez vous derrière un routeur que vous ne pouvez pas configurer (box internet bridée, IP dynamique, NAT carrier-grade). Comment l'administrer à distance ?

Solution classique : port forwarding sur le routeur. Impossible quand vous n'avez pas la main.

Solution moderne : reverse SSH tunnel. La machine cible se connecte d'elle-même vers un VPS public que vous contrôlez (le "bastion"). Une fois la session SSH inverse établie, vous pouvez vous connecter au bastion et rebondir vers la cible.

Architecture :

[Vous] ──SSH──▶ [VPS Bastion] ◀──Reverse SSH── [Machine cible derrière NAT]
                  (IP publique)                    (10.0.0.42)

Prérequis

  • Un VPS public avec SSH actif (= le bastion). Un petit VPS VeryCloud suffit.
  • Une machine cible Linux avec SSH client (Raspberry Pi, NAS, serveur derrière NAT...)
  • Accès root ou sudo sur les deux

Étape 1 : Préparer le bastion

Sur le VPS bastion, créez un user dédié au tunnel :

sudo adduser tunnel
sudo usermod -s /bin/false tunnel  # Pas de shell interactif (sécurité)

Désactivez les fonctions inutiles dans /etc/ssh/sshd_config :

Match User tunnel
    PasswordAuthentication no
    PermitTTY no
    AllowAgentForwarding no
    AllowTcpForwarding yes
    X11Forwarding no
    PermitOpen any
    ForceCommand /bin/false
sudo systemctl reload sshd

Autorisez l'ouverture de ports en écoute (GatewayPorts) :

sudo nano /etc/ssh/sshd_config

Ajoutez :

GatewayPorts clientspecified
sudo systemctl reload sshd

Étape 2 : Générer une clé SSH sur la machine cible

Sur la machine cible (celle derrière le NAT) :

sudo ssh-keygen -t ed25519 -N "" -f /root/.ssh/id_tunnel

Récupérez la clé publique :

cat /root/.ssh/id_tunnel.pub

Étape 3 : Installer la clé sur le bastion

Sur le bastion, ajoutez la clé publique aux authorized_keys du user tunnel :

sudo mkdir -p /home/tunnel/.ssh
sudo nano /home/tunnel/.ssh/authorized_keys

Collez la clé publique. Permissions :

sudo chown -R tunnel:tunnel /home/tunnel/.ssh
sudo chmod 700 /home/tunnel/.ssh
sudo chmod 600 /home/tunnel/.ssh/authorized_keys

Étape 4 : Tester le tunnel manuellement

Sur la machine cible :

sudo ssh -i /root/.ssh/id_tunnel \
    -N -R 2222:localhost:22 \
    tunnel@IP_BASTION

Décrypter :

  • -N : pas d'exécution de commande
  • -R 2222:localhost:22 : ouvre le port 2222 sur le bastion, redirigé vers le port 22 (SSH) de la machine cible

Sur votre poste, connectez-vous au bastion puis rebondissez :

ssh user@IP_BASTION
ssh -p 2222 user_de_la_cible@localhost

Ou directement en une commande depuis votre poste (sans étape intermédiaire) :

ssh -J tunnel@IP_BASTION -p 2222 user_de_la_cible@localhost

Si ça marche, vous êtes connecté à la cible via le bastion.

Étape 5 : Service systemd pour pérenniser le tunnel

Le tunnel manuel meurt au reboot ou à la moindre coupure. On va le rendre persistant.

Sur la machine cible :

sudo nano /etc/systemd/system/reverse-tunnel.service
[Unit]
Description=Reverse SSH tunnel to bastion
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=root
ExecStart=/usr/bin/ssh \
    -NT \
    -o ServerAliveInterval=30 \
    -o ServerAliveCountMax=3 \
    -o ExitOnForwardFailure=yes \
    -o StrictHostKeyChecking=accept-new \
    -i /root/.ssh/id_tunnel \
    -R 2222:localhost:22 \
    tunnel@IP_BASTION
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Activer :

sudo systemctl daemon-reload
sudo systemctl enable --now reverse-tunnel
sudo systemctl status reverse-tunnel

Vérifiez :

sudo journalctl -u reverse-tunnel -f

Sur le bastion :

ss -tlnp | grep 2222
# Doit montrer une écoute sur 127.0.0.1:2222

Étape 6 : Avec autossh (plus robuste que ssh natif)

autossh détecte les coupures et reconnecte plus vite. Installation sur la cible :

sudo apt install -y autossh

Modifiez le service :

sudo nano /etc/systemd/system/reverse-tunnel.service
[Service]
ExecStart=/usr/bin/autossh \
    -M 0 -NT \
    -o ServerAliveInterval=30 \
    -o ServerAliveCountMax=3 \
    -o ExitOnForwardFailure=yes \
    -i /root/.ssh/id_tunnel \
    -R 2222:localhost:22 \
    tunnel@IP_BASTION
sudo systemctl daemon-reload
sudo systemctl restart reverse-tunnel

Étape 7 : Plusieurs tunnels pour plusieurs cibles

Si vous avez 5 Raspberry Pi chez 5 clients, attribuez un port distinct au bastion :

Client Port bastion Cible
ClientA 2201 rpi-a:22
ClientB 2202 rpi-b:22
ClientC 2203 rpi-c:22

Sur chaque cible, adaptez le port dans le -R 22XX:localhost:22.

Pour vous connecter :

ssh -J tunnel@IP_BASTION -p 2202 pi@localhost

Étape 8 : Exposer des ports autres que SSH

Le tunnel reverse n'est pas limité à SSH. Vous pouvez exposer un service web local :

# Sur la cible
ssh -R 8080:localhost:80 tunnel@IP_BASTION

Sur le bastion, le port 8080 redirige vers le serveur web (port 80) de la cible.

Pour le rendre accessible publiquement (pas seulement depuis le bastion), utilisez 0.0.0.0 :

ssh -R 0.0.0.0:8080:localhost:80 tunnel@IP_BASTION

⚠️ Nécessite GatewayPorts clientspecified ou yes côté bastion (déjà fait à l'étape 1).

Étape 9 : Reverse tunnel multi-ports

Pour exposer plusieurs services en un seul tunnel :

ssh -N \
    -R 2222:localhost:22 \
    -R 8080:localhost:80 \
    -R 9090:localhost:9090 \
    tunnel@IP_BASTION

Étape 10 : Nginx en reverse-proxy frontal

Au lieu d'exposer un port 8080 random, faites passer le tunnel par Nginx avec HTTPS et sous-domaine. Sur le bastion :

server {
    listen 443 ssl http2;
    server_name rpi-clienta.verycloud.fr;
    
    ssl_certificate /etc/letsencrypt/live/rpi-clienta.verycloud.fr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/rpi-clienta.verycloud.fr/privkey.pem;
    
    location / {
        proxy_pass http://127.0.0.1:8080;  # le port du tunnel
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Vous accédez maintenant au Raspberry Pi de chez le client A via https://rpi-clienta.verycloud.fr.

Étape 11 : Sécurité supplémentaire

Restreindre les ports exposables

Dans le authorized_keys du user tunnel, préfixez la clé avec des options :

restrict,port-forwarding,permitlisten="2222",no-pty ssh-ed25519 AAAAC3Nz... mathys@laptop

Ce client ne pourra créer des tunnels que sur le port 2222 du bastion. Pratique pour limiter par client.

Firewall sur le bastion

Bloquez l'accès aux ports tunnels depuis l'extérieur :

sudo ufw deny 2222:2299/tcp

Du coup pour vous connecter, vous DEVEZ d'abord passer par SSH sur le bastion (port 22), pas un accès direct depuis internet.

Monitoring du tunnel

Sur la cible :

# Statut
sudo systemctl status reverse-tunnel

# Logs en live
sudo journalctl -u reverse-tunnel -f

Sur le bastion, vérifiez les connexions actives :

ss -tnp | grep tunnel

Étape 12 : Alternative moderne — Tailscale

Pour la même problématique en mode SaaS / zero-config, voir le tuto Tailscale Mesh VPN. Moins de configuration manuelle mais dépendance à un service tiers.

Reverse SSH est :

  • ✅ 100% self-hosted
  • ✅ Minimaliste (juste SSH)
  • ❌ Plus manuel
  • ❌ Nécessite de gérer ports/Nginx

Tailscale est :

  • ✅ Zero-config (clé d'auth et ça marche)
  • ✅ Mesh (chaque machine voit chaque machine)
  • ❌ Dépendance à un service tiers (coordination server)
  • ❌ Plus de couche logicielle

Dépannage

Tunnel ne tient pas

Activez les keepalives (déjà fait dans le service systemd). Vérifiez aussi le NAT/firewall côté cible qui peut couper les connexions inactives. ServerAliveInterval=30 envoie un paquet toutes les 30s.

"Could not request local forwarding"

Le port est déjà utilisé sur le bastion ou GatewayPorts mal configuré. Changez le port ou vérifiez sshd_config.

"Connection refused" depuis le bastion

Le tunnel n'est pas actif. Côté cible :

sudo systemctl status reverse-tunnel
sudo journalctl -u reverse-tunnel -n 30

"Host key verification failed"

Première connexion vers le bastion : SSH demande de valider la fingerprint. Avec StrictHostKeyChecking=accept-new dans le service, c'est auto au premier passage. Si ça échoue, supprimez la ligne du bastion dans /root/.ssh/known_hosts.

Performances dégradées

SSH chiffre tout. Pour de gros transferts, optez pour des ciphers rapides :

ssh -c [email protected] ...

Commandes utiles

# Voir tous les tunnels actifs sur le bastion
ss -tlnp | grep sshd

# Connexions du user tunnel
who | grep tunnel

# Tester la connectivité bastion depuis la cible
sudo -u root ssh -i /root/.ssh/id_tunnel tunnel@IP_BASTION exit
echo $?  # 0 = OK

# Forcer la reconnexion
sudo systemctl restart reverse-tunnel

# Lister les ports exposés depuis le bastion
ss -tlnp | grep '127.0.0.1\|0.0.0.0'

# Killer un tunnel zombie (sur le bastion)
sudo pkill -u tunnel

Conclusion

Le reverse SSH tunnel est l'outil minimaliste pour atteindre n'importe quelle machine derrière un NAT :

  • Pas de port forwarding chez le client/sur la box
  • 100% chiffré (SSH)
  • Persistant via systemd + autossh
  • Multi-tunnel possible avec un seul bastion

Pour aller plus loin :

  • Combinez avec Nginx + Let's Encrypt pour exposer du HTTPS
  • Utilisez Cloudflare Tunnel comme alternative SaaS
  • Migrez vers Tailscale ou WireGuard pour du mesh VPN

Ressources

Join our Discord community server

For any questions, suggestions, or just to chat with the community, join us on Discord!

900+Members