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>
This commit is contained in:
Adam Belkacemi 2026-10-07 10:32:09 +02:00
parent b7e866f868
commit f5a5e8d8a9
13 changed files with 377 additions and 0 deletions

View file

@ -0,0 +1,40 @@
# 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
- [x] `maxretry` relevé de 5 à 10 dans le jail `sshd` (`ansible/roles/durcissement/templates/jail.local.j2`).
- [x] 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.