Logo

TL;DR:

Dans un contexte de homelab, on veut placer une machine Linux derrière une box internet quelconque (dont le plan d'adressage IP interne est inconnu, et qui ne peut pas exposer de port TCP ouvert) et pouvoir quand même y accéder depuis Internet.

C'est donc cette machine qui va initier la connexion vers l'extérieur pour établir un tunnel SSH, lequel servira à faire passer tout le traffic.

Le use-case est d'avoir un serveur de secours pour son homelab, qu'on place chez un tiers.

Concepts:

Tunneling, port forwarding, port knocking

Outils:

Ubuntu, SSH, ufw, knockd

Sommaire

Introduction

Procédure

Contexte

Dans l'exemple qui suit on a deux serveurs distants qu'on contrôle:

serveurA
serveurB
Deux serveurs distants
(image générée par IA)

serveurB est complètement isolé, derrière un firewall stateful (il peut initier des connexions vers l'extérieur mais n'expose aucun service).

serveurA expose son service SSH sur internet (c'est juste un exemple pour illustrer le propos, on verra plus loin comment protéger l'accès au service SSH car au final il faut bien que quelqu'un expose quelque chose).

Introduction

Pourquoi pas un VPN ?

On pourrait partir sur une "vraie" solution VPN (WireGuard) mais on a pour contrainte le fait qu'un des serveurs ne peut pas exposer de port ouvert sur une IP publique.

Objectif: ne dépendre que d'un SSH sortant.

Etablir un tunnel SSH est fort simple, il n'y a qu'une commande à passer. Par exemple, si on a notre machine serveurB qui peut accéder à serveurA mais uniquement en SSH (tous les autres ports sont filtrés), on tape sur serveurB:

(serveurB) $ ssh -L 12345:serveurA:8080 user@serveurA

Et serveurB ouvre une session SSH vers serveurA + ouvre également le port TCP 12345 en écoute locale (-L = local), qui est redirigé vers le port 8080 de serveurA.

=> Résultat: maintenant, on peut se connecter sur le port 12345 de serveurB pour accéder au 8080/tcp de serveurA.

Note

On aurait pu mettre 8080:serveurA:8080 pour ouvrir sur serveurB le même numéro de port TCP mais pour la clarté de la syntaxe j'ai mis deux ports différents.

Finalité

Là où ça devient intéressant c'est quand serveurA n'a pas accès du tout à serveurB (tous les ports sont filtrés) mais qu'on veut quand même pouvoir se connecter depuis serveurA à serveurB sur n'importe quel port en passant par un tunnel SSH:

Depuis serveurB on peut ouvrir un port distant en utilisant -R (Remote) au lieu de -L (Local). Par exemple:

(serveurB) $ ssh -R 2222:localhost:22 user@serveurA

Cette fois, serveurB ouvre une session SSH vers serveurA + ouvre également le port TCP 2222 en écoute sur la cible (-R = remote), qui est redirigé vers le port 22 de serveurB.

L'intérêt c'est que désormais, sur serveurA on peut taper ça pour se connecter à serveurB:

(serveurA) $ ssh -p 2222 localhost

=> Ce qui aura pour effet de se connecter en SSH sur serveurB.

=> Résultat: maintenant, depuis serveurA on peut atteindre n'importe quel port qui écoute sur serveurB en utilisant une autre redirection par tunnel SSH au moment de se connecter. Par exemple:

(serveurA) $ ssh -L 1234:localhost:1234 user@localhost -p 2222

=> Avec cette commande, on ouvre le port 1234 en écoute locale sur serveurA, et en se connectant dessus c'est comme si on atteignait le port 1234 sur serveurB.

Variantes

On peut combiner les deux modes Local et Remote, en mettant "-L" et "-R" dans une seule commande de connexion.

On peut ouvrir plusieurs ports d'un coup en mettant plusieurs "-L" et/ou plusieurs "-R" à la suite, dans une seule commande.

On peut ajouter l'option "-N" pour établir le tunnel sans ouvrir de session shell.

Toute la problématique est donc de mettre en place un tunnel qui s'établit automatiquement au démarrage, qui se maintient ouvert avec un keepalive, qui se réétablit en cas de coupure réseau ou de redémarrage d'une des deux machines, et qui fait des retry en cas d'échec. C'est cette logique qui est exposée ici.

Procédure

Le but est de pouvoir se connecter à un PC distant, en DHCP derrière une box internet inconnue (avec une IP inconnue sur son réseau local). C'est donc ce dernier qui va initier la connexion, vérifier régulièrement qu'elle est up, et la relancer si nécessaire.

Contexte

Dans l'exemple anonymisé qui suit on a toujours le serveur local serveurA qui doit pouvoir se connecter sur le serveur distant serveurB.

On part du principe qu'initialement, le firewall est actif et que tous les ports sont filtrés sur serveurA.

1/ ajout d'une seconde instance sshd sur serveurA

On va configurer une deuxième instance du serveur SSHd, sur un autre port, dédiée à gérer ce tunnel.

Choisir un port pour le nouveau service SSH, par exemple: 56789.

Copier puis éditer le fichier de configuration de SSHd:

# cp /etc/ssh/sshd_config /etc/ssh/sshd_config_reverse
# vi /etc/ssh/sshd_config_reverse

Mettre à la fin (on reprend le port cible choisi précédemment):

Port 56789
PidFile /var/run/sshd_reverse.pid
ListenAddress 0.0.0.0
PermitRootLogin no
PasswordAuthentication no
AllowTcpForwarding yes
GatewayPorts yes

l'option GatewayPorts yes

Ce paramètre permet à un port ouvert par l'option "-R" (remote) d'écouter sur toutes les interfaces (pas seulement localhost). En ce qui me concerne c'est voulu, mais c'est une surface d'attaque supplémentaire donc attention à ne l'utiliser que lorsque c'est nécessaire.

# cp /lib/systemd/system/ssh.service /etc/systemd/system/sshd-reverse.service
# vi /etc/systemd/system/sshd-reverse.service

Adapter ces lignes au nouveau service:

ExecStart=/usr/sbin/sshd -D -f /etc/ssh/sshd_config_reverse
Alias=sshd_reverse.service

Démarrer le service sshd n°2:

# systemctl daemon-reexec
# systemctl daemon-reload
# systemctl enable sshd-reverse
# systemctl start sshd-reverse

2/ Configuration du port knocking sur serveurA

Pour que la machine serveurB initie la connexion vers serveurA et établisse le tunnel SSH, on configure sur serveurA un port knocking préalable.

Notes

1. Il ne s'agit pas de considérer la séquence de port knocking comme réellement "secrète" (elle est dévoilée sur le réseau à chaque connexion) mais plutôt d'éviter de pourrir les logs avec plein de tentatives de connexion.

2. Dans mon cas j'ai une box internet qui fait de la redirection de ports vers un routeur séparé, qui fait à son tour de la redirection de ports vers le réseau interne, et une VM avec une IP sur le network bridge, donc il faut configurer sur la box internet, puis sur le routeur séparé, chaque port à rediriger de la séquence.

Configurer sur serveurA le port knocking:

Note

Dans l'exemple qui suite on choisit pour séquence de port-knocking: 12345, 23456, 34567, 45678.

A modifier selon ses préférences (on peut définir une séquence de 50 ports si on a envie).

# apt install knockd
# echo "KNOCKD_OPTS=\"-i enp1s0\"" >>/etc/default/knockd

Remplacer enp1s0 par le nom de l'interface exposée.

# vi /etc/knockd.conf
[SSH]
    sequence      = 12345,23456,34567,45678
    seq_timeout   = 30
    start_command = ufw insert 1 allow from %IP% to any port 56789
    tcpflags      = syn
    cmd_timeout   = 30
    stop_command  = ufw delete allow from %IP% to any port 56789
# service knockd restart

Les logs sont dans: /var/log/knockd.log

3/ Ajout d'un compte user remote (toujours sur serveurA)

On crée un user dédié à établir la connexion:

# useradd -m -d /home/remote -s /bin/bash remote
# su - remote
$ mkdir .ssh
$ chmod 700 .ssh
$ cd .ssh
$ touch authorized_keys
$ chmod 600 authorized_keys

Copier la clé ssh publique du compte utilisateur sur serveurB dans authorized_keys.

Suite: à faire sur serveurA une fois que le setup est terminé:

# usermod -s /bin/false remote

4/ Côté client (serveurB)

# useradd -m -d /home/remote -s /bin/bash remote
# su - remote
$ mkdir .ssh
$ chmod 700 .ssh
$ ssh-keygen -t ed25519
$ cat .ssh/id_ed25519.pub

(copier cette clé publique sur serveurA dans ~remote/.ssh/authorized_keys)

$ exit
# apt install knockd

Connexion simple: d'abord on port-knocke:

$ knock -d 10 serveurA 12345,23456,34567,45678

Puis on peut ouvrir la connexion pendant les 30 secondes qui suivent (le timeout défini dans la conf de knockd):

$ ssh -p 56789 remote@serveurA

5/ Test du tunnel ssh

Sur serveurB:

$ knock -d 10 serveurA 12345 23456 34567 45678
$ ssh -N -R 2222:localhost:22 remote@serveurA -p 56789
Troubleshooting

En cas de problème, vérifier les logs sur serveurA:

# tail -f /var/log/knockd.log

Utilisation du tunnel

Exemple 1: Connexion SSH simple

Sur serveurA:

On peut donc se connecter à serveurB en tapant:

$ ssh localhost -p 2222

Mais pour pouvoir se connecter de façon transparente, éditer le fichier ~/.ssh/config et ajouter les informations de connexion suivantes:

Host serveurB
  HostName localhost
  Port 2222
  User thom
  ProxyJump thom@localhost

De la sorte, on peut se connecter en SSH de façon plus "normale":

$ ssh serveurB
Exemple 2: Connexion SSH à une VM hébergée sur serveurB

Même chose pour pouvoir se connecter à une VM hébergée sur serveurB, on pourra faire un double rebond en mettant cette configuration dans ~/.ssh/config:

Host serveurB
  HostName localhost
  Port 2222
  User thom
  ProxyJump thom@localhost
Host vm_toto
  HostName vm_toto
  ProxyJump serveurB
  LocalForward 8080 localhost:8080
  LocalForward 8443 localhost:8443
  ForwardX11 yes

Ainsi, on peut se connecter à la VM "vm_toto" sur serveurB en tapant juste:

$ ssh vm_toto

depuis serveurA. Et dans cet exemple, on aura deux redirections de ports supplémentaires pour pouvoir se connecter, dans cet exemple, au 8080 et au 8443 depuis serveurA en tapant sur localhost. Ici la simple commande "ssh vm_toto" revient à faire:

$ ssh -o ProxyJump=serveurB -N -L 8080:localhost:8080 -L 8443:localhost:8443 vm_toto
Exemple 3: Sauvegarde distante

Autre use-case: pour sauvegarder des données de serveurA sur serveurB en passant par le tunnel SSH:

$ rsync -avz --delete -e ssh /data/ serveurB:/hdd/backup/ 

6/ Maintien du tunnel up

Script: check_tunnel.sh sur sur serveurB

Ce script ouvre le tunnel SSH, vérifie qu'il est up et le relance si nécessaire (on reprend tous les paramètres de l'exemple détaillé précédemment):

Note

On peut aussi utiliser autossh qui sert à faire la même chose. Simplement, je trouve la solution suivante (script perso + cron + keepalive) plus basique et je peux dire qu'elle est très robuste après environ 1 an de fonctionnement.

#!/bin/bash
REMOTE_USER=remote
REMOTE_HOST=serveurA
REMOTE_PORT=56789
REVERSE_PORT=2222
LOCAL_PORT=22
PK_SEQUENCE="12345 23456 34567 45678"
KEEPALIVE_FILE=/home/remote/keepalive
MAX_AGE=120

# launch tunnel
if [ "$1" = "-s" ] ;then
  /usr/bin/knock -d 10 $REMOTE_HOST $PK_SEQUENCE
  /usr/bin/pkill -u $REMOTE_USER
  sleep 0.5
  /usr/bin/su - $REMOTE_USER -c "/usr/bin/ssh -N -f -R $REVERSE_PORT:localhost:$LOCAL_PORT $REMOTE_USER@$REMOTE_HOST -p $REMOTE_PORT"
fi

# check keepalive / launch tunnel if down
if [ "$1" = "-c" ] ;then
  if [ -f "$KEEPALIVE_FILE" ]; then
    AGE=$(($(date +%s) - $(stat -c %Y "$KEEPALIVE_FILE")))
    [ "$AGE" -ge "$MAX_AGE" ] && /root/tunnel.sh -s && touch $KEEPALIVE_FILE
  fi
fi
#end#

Mettre ce script toutes les minutes dans la crontab pour maintenir le lien:

# crontab -e
* * * * * /root/tunnel.sh -c >>/root/tunnel.log 2>&1
Implémenter le touch du fichier keepalive depuis serveurA

Ceci est nécessaire pour que serveurB sache que serveurA a toujours accès au tunnel.

Le principe: la machine serveurA se connecte une fois par minute sur serveurB pour faire un touch d'un fichier.

En tant que remote@serveurA: générer une clé RSA et copier la clé publique sur serveurB dans /home/remote/.ssh/authorized_keys.

Mettre la commande de keepalive dans la crontab:

# crontab -e
* * * * * timeout 10 ssh -p 2222 remote@localhost "touch keepalive"

--
TG - 08/2025

Cet article a été généré par un humain