Paillasse/docs/postmortems/2026-10-07-fail2ban-auto-ban.md
Adam Belkacemi f5a5e8d8a9 durcit le serveur : pare-feu nftables, SSH par clé, fail2ban, chrony, mises à jour de sécurité
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-07 10:32:09 +02:00

5.6 KiB

Post-mortem — auto-bannissement fail2ban pendant le durcissement

  • Date : 2026-10-07
  • Durée de l'incident : ~1 h (accès SSH restauré manuellement au bout de quelques minutes via la console Scaleway)
  • Gravité : faible — aucune perte de données, aucune coupure de service (rien n'est encore en production), juste une perte temporaire d'accès SSH depuis le Mac d'Adam.
  • Impact : Claude Code ne pouvait plus se connecter en SSH au serveur Syllabus depuis le Mac d'Adam.

Résumé

Pendant la mise au point du rôle Ansible durcissement (étape 2 du déploiement), le playbook a été relancé plusieurs fois en quelques minutes pour corriger des bugs (voir plus bas) et vérifier l'idempotence. Chaque exécution du playbook tente volontairement une connexion avec mot de passe et une connexion root, pour vérifier qu'elles sont bien refusées. Ces tentatives volontairement ratées ont été comptées par fail2ban comme des échecs d'authentification. Avec le seuil initial (maxretry = 5 sur findtime = 10m), l'IP publique du Mac d'Adam a dépassé ce seuil et s'est fait bannir par son propre pare-feu, pendant bantime = 1h.

Chronologie

  1. 1er run du playbook : bug découvert (le notify pour recharger sshd manquait sur la tâche de déploiement de la config SSH). Le pare-feu a été appliqué mais SSH non rechargé → le test « root refusé » échoue réellement (root pouvait encore se connecter). Le filet de sécurité (timer de 5 min) n'est jamais désarmé → il se déclenche et remet tout à l'état d'origine. Aucune conséquence négative : c'est exactement le rôle de ce filet.
  2. Correction du bug, 2ᵉ run : le test nmap (-p-, les 65535 ports) s'est révélé trop lent face à une politique de pare-feu drop (chaque port fermé attend un timeout au lieu d'un rejet immédiat). Le run dépasse les 5 minutes du filet de sécurité avant d'arriver à l'étape de désarmement → 2ᵉ déclenchement automatique du rollback. Aucune conséquence négative, même cause (filet de sécurité qui fait son travail).
  3. Correction du scan nmap (ports par défaut + -T4, 4,5 s au lieu de plusieurs minutes), 3ᵉ run : succès complet, toutes les vérifications passent, le filet est désarmé normalement.
  4. Relance d'un 4ᵉ run pour vérifier l'idempotence (bonne pratique, règle 7 du prompt de déploiement) : SSH refuse la connexion (Connection refused, pas un timeout). Le ping fonctionne : le serveur est up, seul le port 22 est concerné.
  5. Diagnostic : fail2ban a banni l'IP publique du Mac (89.86.35.23), déclenché par l'accumulation des tentatives « mot de passe refusé » et « root refusé » des runs 1 à 3, toutes dans la même fenêtre findtime de 10 minutes.
  6. Résolution : Adam s'est connecté à la console web Scaleway (hors bande, sans dépendre du réseau/SSH) et a lancé sudo fail2ban-client set sshd unbanip 89.86.35.23 directement sur le serveur. Accès SSH restauré immédiatement.
  7. Correctif durable : maxretry relevé de 5 à 10 dans ansible/roles/durcissement/templates/jail.local.j2, pour tolérer les futures vérifications répétées sans affaiblir significativement la protection contre un vrai bruteforce.

Cause racine

Le seuil de bannissement de fail2ban ne tenait pas compte du fait que le playbook de durcissement lui-même génère des échecs d'authentification volontaires à chaque exécution, pour vérifier que les protections fonctionnent. Plusieurs exécutions rapprochées (pendant la mise au point) ont donc eu le même effet qu'une attaque par force brute à faible volume, et fail2ban a réagi exactement comme prévu — contre nous.

Ce qui a bien fonctionné

  • Le filet de sécurité anti-coupure (timer systemd-run de 5 minutes, règle 4 du prompt de déploiement) a fonctionné parfaitement aux essais 1 et 2 : aucune coupure réelle, retour automatique et propre à l'état précédent, confirmé dans les journaux (journalctl -t durcissement-rollback).
  • fail2ban a démontré qu'il fonctionne (il a banni une IP suite à des échecs répétés), même si c'était la mauvaise IP.
  • La console web du fournisseur (hors bande, indépendante du réseau du serveur) a permis de résoudre l'incident sans attendre l'expiration du bannissement.

Actions correctives

  • maxretry relevé de 5 à 10 dans le jail sshd (ansible/roles/durcissement/templates/jail.local.j2).
  • Scan nmap de vérification limité aux ports par défaut (au lieu de -p-), avec -T4 : 4,5 s au lieu de plusieurs minutes, pour rester largement dans la fenêtre du filet de sécurité.
  • À envisager plus tard (pas fait maintenant, pas nécessaire dans l'immédiat) : un jail ou une règle dédiée qui ignore les tentatives provenant du processus de vérification Ansible lui-même, si le besoin se reproduit.

Comprendre

Fail2ban compte les échecs de connexion SSH, peu importe qui les fait : un vrai attaquant ou nos propres tests. Comme le rôle de durcissement vérifie activement que le mot de passe et root sont refusés — donc provoque volontairement des échecs — relancer ce rôle plusieurs fois en peu de temps ressemble, du point de vue de fail2ban, à une attaque. C'est un classique en administration système : les outils de protection automatique protègent contre tout le monde, y compris leurs propres opérateurs. La leçon à retenir : quand on écrit un test qui provoque volontairement un échec d'authentification, il faut aussi réfléchir à l'effet cumulé de ce test sur les systèmes de détection d'intrusion, surtout si le test va être rejoué souvent pendant la mise au point.