Contexte / besoin
Le serveur de production auto-hébergé (sites clients, reverse proxy, services internes) devait passer une maintenance planifiée : mises à jour de sécurité et redémarrage, le premier depuis des mois, avec un noyau en attente et une mémoire saturée par le long uptime. Au reboot, tous les services conteneurisés repartent seuls (politiques de redémarrage Docker). SSH, lui, refuse toute connexion.
Diagnostic
Trois observations circonscrivent le problème avant même d’ouvrir une console :
- le refus est immédiat (« Connection refused », sans timeout) : la machine est jointe, mais rien n’écoute sur le port SSH ;
- les sites sont en ligne : Docker et le réseau vont bien ;
- le pair répond sur le maillage Tailscale : le démon VPN tourne. Le mort, c’est sshd seul.
Restait la console KVM out-of-band du serveur (et son clavier interprété
en QWERTY, exercice d’humilité en soi). Verdict de systemctl status ssh,
adresse expurgée :
sshd: error: Bind to <ip-vpn> failed: Cannot assign requested address.
sshd: fatal: Cannot bind any address.
Cause racine : sshd_config contient un ListenAddress restreint à
l’adresse du VPN maillé. C’est un choix de durcissement délibéré
(réseau zero-trust) : SSH n’est joignable que depuis
le réseau privé. Or au boot, sshd démarre avant que Tailscale n’ait monté
son interface : l’adresse n’existe pas encore, le bind échoue, sshd épuise
ses tentatives et reste couché. Le serveur n’avait jamais redémarré depuis
ce durcissement : la panne était en place depuis des mois, invisible.
Contraintes
- Machine de production avec des clients dessus : ni réinstallation, ni improvisation.
- Le correctif ne doit rien céder sur la posture : SSH doit rester joignable uniquement via le VPN.
- Le chemin de secours doit rester simple : c’est lui qu’on emprunte quand tout le reste est cassé.
Options envisagées
- Écouter sur toutes les interfaces, filtrer au pare-feu sur l’interface VPN : robuste face au problème d’ordre de démarrage, mais la restriction quitte le bind pour le pare-feu, et une erreur de règle expose le port.
- Ordonnancement systemd (
After=le démon VPN, plus une boucle d’attente de l’adresse) : fragile, un service marqué démarré n’a pas forcément son adresse déjà assignée ; il faut scripter l’attente et gérer le timeout. - Faire transiter SSH par le reverse proxy (passthrough TCP) : rejeté d’emblée. Le chemin de secours dépendrait alors de Docker et du reverse proxy ; on ne répond pas à une panne de dépendance de démarrage en ajoutant deux dépendances.
net.ipv4.ip_nonlocal_bind=1(sysctl) : autorise un processus à se binder sur une adresse qui n’existe pas encore. sshd démarre toujours, et n’écoute toujours que sur l’adresse du VPN.
Décision & pourquoi
L’option 4 : le changement minimal qui corrige l’ordre de démarrage sans
toucher à la posture de sécurité, une ligne dans /etc/sysctl.d/.
Complétée par un test de non-régression qui reproduit les conditions du
boot :
systemctl stop <vpn> && systemctl restart ssh && systemctl is-active ssh
Si sshd est active alors que le VPN est coupé, la course au démarrage ne
peut plus se reproduire. En parallèle, une seconde porte de secours
indépendante est conservée (la console out-of-band) : les deux mécanismes
ne partagent aucune dépendance.
Tradeoff assumé
ip_nonlocal_bind est un réglage global : n’importe quel processus peut
désormais se binder sur une adresse non locale. Acceptable sur un serveur
mono-administrateur ; l’alternative pare-feu (option 1) reste documentée
si le contexte change.
Résultat
SSH survit au redémarrage quel que soit l’ordre de démarrage des services. Et trois leçons durables :
- Un service qui tourne depuis des mois n’a jamais prouvé qu’il savait démarrer. Seul un reboot le prouve ; d’où l’intérêt d’en planifier régulièrement, sans attendre que le noyau l’exige.
- Le chemin de secours doit dépendre du minimum de choses. Chaque dépendance ajoutée est une raison de plus de se retrouver devant une console KVM à minuit.
- Le durcissement crée des couplages. Restreindre un service à une interface, c’est le coupler au cycle de vie de cette interface : un couplage qui se teste.
L’incident est résumé au journal d’exploitation.