
🌐 Aussi en: English · Deutsch · Español
Deep Dive de la série autour de l’Ansible Tour. Niveau : administration Linux avancée. Le Tour explique les rôles ; les deep dives expliquent la technique en dessous – comment elle fonctionne, pourquoi elle est là, ce que cette installation en fait exactement, comment l’exploiter au quotidien et quelles autres options il y aurait eu.
fail2ban ressemble à l’outil le plus simple de ces serveurs : lire un journal, compter les échecs, bannir l’adresse. Sur un hôte K3s, chaque partie de cette phrase a un piège. Les journaux vivent dans des fichiers de conteneurs qui changent de nom à chaque redémarrage ; les bannissements doivent atterrir dans un pare-feu où d’autres programmes écrivent aussi ; et une famille d’adresses que personne n’a testée peut transformer le tout en placebo. Cet article suit une seule requête malveillante, de la ligne de journal jusqu’à l’entrée dans le noyau, explique ce que la partie 5 de la visite ne fait qu’effleurer – et décrit trois cas où fail2ban annonçait « Ban » alors que le pare-feu laissait entrer l’adresse.
🧭 Comment fonctionne fail2ban
Jails, filtres, actions
Une jail relie trois éléments : un journal à surveiller, un filtre qui reconnaît une mauvaise ligne et une action qui bannit. Le filtre est un ensemble d’expressions régulières (failregex, avec ignoreregex pour les exceptions) où <HOST> marque l’adresse. Chaque correspondance est un échec pour cette adresse. Si une adresse accumule maxretry échecs en findtime, l’action s’exécute et la bannit pour bantime.
[nginx-k3s-wp-login]
enabled = true
filter = nginx-k3s-wp-login
logpath = /var/log/pods/ingress-nginx_ingress-nginx-*/nginx-ingress/*.log
backend = polling
maxretry = 10
findtime = 600Backends : comment le journal est lu
- systemd lit directement le journal (filtré par unité) – robuste, sans fichiers ni rotation. Utilisé ici pour
sshd. - polling vérifie le fichier toutes les quelques secondes. Simple, fonctionne sur tout système de fichiers – et seul choix possible quand le chemin est un glob qui change.
- pyinotify réagit immédiatement aux événements de fichiers ; plus rapide, mais il surveille les fichiers qui existaient au démarrage de la jail.
L’horodatage compte autant que l’adresse : fail2ban lit la date de chaque ligne (datepattern) et ignore les lignes plus anciennes que findtime. Un filtre au mauvais format de date ne trouve rien – en silence.
La base de données et recidive
fail2ban enregistre les bannissements en cours dans une base SQLite (/var/lib/fail2ban/fail2ban.sqlite3). Après un redémarrage, il réapplique chaque bannissement non expiré. La jail recidive retourne fail2ban contre lui-même : son journal est fail2ban.log, son filtre compte les lignes « Ban », et quiconque est banni trois fois dans la journée l’est pour 30 jours sur tous les ports.
🛡️ Pourquoi il est là
- Une protection peu coûteuse contre les attaques automatisées sur SSH, la connexion WordPress, la connexion Nextcloud et Grafana – et contre les scanners qui parcourent des listes de chemins vulnérables connus.
- Des bannissements dans le noyau, pas dans l’application. Une adresse bannie n’atteint rien du tout, pas même la poignée de main TLS.
- De la visibilité. Qui frappe, combien de fois et à quelle porte figure dans le journal des bannissements – consultable dans Loki depuis octobre 2026.
🔧 Ce que fait ce dépôt
Sept jails, deux sources de journaux
| Jail | Journal | Règle | Bannissement |
|---|---|---|---|
| sshd | journal (backend systemd) | 3 échecs | par défaut 1 h |
| nginx-k3s-wp-login | journal de l’ingress | 10 POST vers /wp-login.php en 10 min | 1 h |
| nginx-k3s-nc-login | journal de l’ingress | 10 POST vers /login de Nextcloud en 10 min | 1 h |
| nginx-k3s-grafana-login | journal de l’ingress | 5 connexions Grafana échouées (HTTP 401) en 10 min | 1 h |
| nginx-k3s-scanner | journal de l’ingress | 20 réponses 403 ou 404 en 1 min | 1 h |
| nginx-k3s-bad-paths | journal de l’ingress | 2 requêtes vers des chemins qui n’existent jamais ici (/.env, /.git/, /phpmyadmin, web shells …) en 1 h | 24 h |
| recidive | fail2ban.log | banni 3 fois dans la journée | 30 jours |
Toutes les jails HTTP lisent le journal d’accès du contrôleur ingress, le seul endroit par lequel passe chaque requête, pour le blog comme pour Nextcloud. Les filtres attendent le préfixe de containerd devant chaque ligne (2026-06-19T14:24:11Z stdout F <client> - - [heure] "POST /wp-login.php …") ; un filtre écrit pour un journal nginx ordinaire ne correspondrait jamais.
Le bannissement va directement dans nftables
fail2ban n’a ni table ni chaîne à lui. Son action ajoute l’adresse à un set que le pare-feu vérifie tout en haut de sa chaîne input – chaque jail dans sa propre paire f2b_<jail>4/6 (pourquoi, voir le cinquième bug plus bas) :
[Definition]
actionban = nft add element inet filter <nftset> { <ip> timeout <bantime>s }
actionunban = nft delete element inet filter <nftset> { <ip> } 2>/dev/null; true
[Init]
setname = banned
nftset = <setname>4
[Init?family=inet6]
nftset = <setname>6
# jail.local - every jail names its own set pair
[sshd]
action = nftables-banned-sets[setname=f2b_sshd]Chaque élément porte sa propre expiration ; un bannissement expire donc dans le noyau même si fail2ban est mort entre-temps. Le débannissement n’est qu’un nettoyage au mieux.
Le fichier de journal qui se déplace
Le journal de l’ingress n’est pas un fichier unique. C’est /var/log/pods/<namespace>_<pod>_<uid>/nginx-ingress/N.log, et le répertoire du pod comme le numéro changent : un redémarrage de conteneur écrit 1.log après 0.log, un nouveau pod reçoit un nouveau répertoire. fail2ban développe le glob de logpath une seule fois, au démarrage de la jail. Un petit timer (fail2ban-ingress-logwatch, toutes les deux minutes) compare la liste actuelle des fichiers à la précédente et recharge les jails nginx-k3s-* si elle a changé. Sans lui : voir le premier bug ci-dessous.
🐛 Trois fois fail2ban a dit « Ban » et rien ne s’est passé
1. Surveiller un fichier mort pendant des jours
Le 23 septembre 2026, il est apparu que les jails HTTP des deux serveurs Nextcloud n’avaient rien banni depuis des jours. Après un redémarrage, fail2ban avait démarré avant le conteneur ingress, et après un redémarrage de conteneur les jails continuaient de suivre un fichier qui ne recevait plus de lignes. Tout semblait sain – le service tournait, les jails étaient listées, aucune erreur nulle part. Le timer décrit plus haut est la correction. C’est aussi l’une des raisons pour lesquelles Monit vérifie désormais que chaque jail attendue est active (l’histoire du chien de garde a commencé par un fail2ban mort en silence pendant deux jours).
2. Un rechargement du pare-feu qui vidait les sets
Le pare-feu remplace sa propre table à chaque rechargement – et avec elle les sets, vides. fail2ban gardait ses bannissements dans sa base et les croyait actifs. Le 30 septembre 2026, après une exécution du playbook, 15 bannissements sur 17 sur l’un des serveurs et 17 sur 24 sur l’autre n’existaient plus que dans fail2ban. Le handler du pare-feu redémarre maintenant fail2ban juste après chaque rechargement, et fail2ban réapplique chaque bannissement non expiré.
3. Des bannissements IPv6 qui n’ont jamais été possibles
Jusqu’au 14 octobre 2026, chaque jail passait nftset = banned4 à l’action – y compris pour les adresses IPv6. nft refusait de mettre une adresse IPv6 dans un set IPv4 (« Address family for hostname not supported »), fail2ban journalisait quand même le bannissement, et l’adresse n’était jamais bloquée. Sur les trois serveurs. La section [Init?family=inet6] ci-dessus choisit le set selon la famille d’adresses ; dans la semaine jusqu’au 20 octobre 2026, 15 bannissements sur 444 concernaient des adresses IPv6 – peu, mais chacune serait auparavant entrée sans encombre.
Un quatrième, trouvé par Loki
Quand les journaux sont partis vers Loki le 19 octobre 2026, la nouvelle alerte sur les erreurs de fail2ban s’est déclenchée aussitôt. La plupart des erreurs étaient inoffensives (KeyError sur un journal de pod tout juste supprimé – le backend polling perd un fichier, la jail continue). Mais les 13 et 14 octobre, il y en avait de vraies : Failed to execute ban jail 'nginx-k3s-bad-paths' action 'nftables…' – des bannissements qui n’ont pas eu lieu pendant que le rechargement du pare-feu était remanié. Personne ne les avait vues, car fail2ban n’envoie pas ses propres erreurs par e-mail. L’alerte le fait désormais, motif inoffensif connu excepté.
Un cinquième : un seul set pour toutes les jails
Jusqu’au 21 octobre 2026, toutes les jails écrivaient dans la même paire de sets banned4/6. Cela semble ordonné, mais il y a un piège : une adresse bannie par deux jails n’existe qu’une fois dans le set. Quand le bannissement le plus court prenait fin – celui de la jail scanner au bout d’un jour, par exemple –, l’adresse quittait le set, alors que recidive voulait encore la tenir dehors 30 jours. fail2ban la listait comme bannie, le noyau non. Une comparaison ce jour-là a trouvé une à cinq adresses de ce genre par serveur. Depuis, chaque jail a sa propre paire (f2b_sshd4, f2b_recidive4, …) et n’ajoute et ne retire que là, et un contrôle Monit (f2b_sets) compare régulièrement la liste de fail2ban avec les sets.
📊 Ce qu’il bannit
Une semaine, du 14 au 20 octobre 2026, sur les trois serveurs :
| Jail | Blog | Nextcloud A | Nextcloud B |
|---|---|---|---|
| nginx-k3s-bad-paths | 107 | 74 | 74 |
| nginx-k3s-scanner | 63 | 50 | 47 |
| sshd | 9 | 2 | 11 |
| recidive | 4 | 2 | – |
| nginx-k3s-wp-login | 1 | – | – |
| Part d’IPv6 | 11 sur 184 | 2 sur 128 | 2 sur 132 |
Le tableau est clair : presque tous les bannissements viennent de scanners qui cherchent des fichiers .env, des répertoires Git, des panneaux d’administration et des web shells. Les vraies attaques de connexion sont rares – le jour de l’analyse, la connexion WordPress a vu deux POST en trois heures, et depuis le 19 octobre 2026 /wp-login.php ne répond de toute façon qu’aux adresses de la liste de pays. Les bannissements SSH sont peu nombreux parce que le port 10022 n’accepte que des plages d’adresses allemandes ; il reste quelques adresses venant de cette plage.
🧰 Exploitation au quotidien
# toutes les jails, puis une en détail (actuellement bannis, total)
fail2ban-client status
fail2ban-client status nginx-k3s-bad-paths
# la vue du pare-feu : chaque jail a sa propre paire de sets f2b_<jail>4/6
nft list set inet filter f2b_bad_paths4
nft list set inet filter f2b_recidive4
fail2ban-client get sshd action nftables-banned-sets setname # -> f2b_sshd
# lever un bannissement (les deux : l'entrée de fail2ban et le set du noyau)
fail2ban-client set nginx-k3s-scanner unbanip 203.0.113.7
# un filtre correspond-il à de vraies lignes ? (sur le journal ingress actuel)
fail2ban-regex /var/log/pods/ingress-nginx_ingress-nginx-*/nginx-ingress/0.log \
/etc/fail2ban/filter.d/nginx-k3s-bad-paths.conf
# quel fichier une jail surveille-t-elle en ce moment ?
fail2ban-client get nginx-k3s-wp-login logpathQuand une jail ne se déclenche pas
- Surveille-t-elle le bon fichier ?
get <jail> logpathface àls /var/log/pods/…– après un redémarrage du contrôleur ingress, le timer devrait l’avoir rechargée en deux minutes. - Le filtre correspond-il ?
fail2ban-regexsur le vrai fichier. Un changement de format de ligne après une mise à jour de l’ingress apparaît ici. - La date est-elle comprise ? Les lignes plus anciennes que
findtimesont ignorées ; un mauvaisdatepatternrend chaque ligne « ancienne ». - Le bannissement atteint-il le noyau ? Comparer
statusavecnft list set. S’ils diffèrent, chercher « Failed to execute ban » dansfail2ban.log– ou dans Loki :{job="fail2ban"} |= "ERROR".
Ne pas tester depuis sa propre machine de rebond
Demander une fois /.env pour voir si la jail bad-paths fonctionne est tentant. Deux requêtes en une heure bannissent l’adresse pour 24 heures – y compris la machine de rebond, et avec elle toutes les sessions SSH vers ce serveur. Les plages privées sont dans ignoreip, pas l’adresse publique du réseau d’administration. Tester avec fail2ban-regex sur une ligne de journal, ou depuis une adresse dont on peut se passer une journée.
🆘 Antisèche : symptôme → commande
La version courte de cet article, pour le moment où quelque chose ne va pas. À lancer sur l’hôte en root ; <…> sont des espaces réservés.
| Symptôme | Commande |
|---|---|
| Une jail ne bannit rien | fail2ban-client status <jail> |
| Le filtre ne correspond pas | fail2ban-regex <logfile> /etc/fail2ban/filter.d/<filter>.conf |
| Banni, mais passe quand même | nft get element inet filter f2b_<jail>4 '{ <ip> }' # "No such file" = not in the set |
| Dans quel set écrit une jail ? | fail2ban-client get <jail> action nftables-banned-sets setname |
| Lever un bannissement | fail2ban-client unban <ip> |
| Un bannissement a-t-il échoué ? | grep -E "ERROR|Failed to execute" /var/log/fail2ban.log | tail |
| fail2ban est arrêté, ou une jail manque | systemctl status fail2ban |
| Sets vides après un rechargement du pare-feu | systemctl restart fail2ban # re-applies every active ban |
| Bloqué soi-même | # from another address or the provider console: |
🔭 La suite
- Les bannissements dans Grafana – fait. Depuis le 21 octobre 2026, le dépôt fournit un tableau de bord « fail2ban – Bans » : bannissements par jail et par heure, totaux pour la période choisie et dernières lignes de bannissement, le tout à partir des lignes que Loki a déjà.
- Une liste de bannissement pour les trois serveurs. Un scanner banni sur le blog est souvent le même qui essaie les serveurs Nextcloud une heure plus tard. Le partage demanderait une petite synchronisation entre les hôtes.
- Des listes de réputation (à la manière de CrowdSec) comme couche supplémentaire – au prix d’un service externe.
⚖️ Alternatives – et pourquoi fail2ban
| Option | Point fort | Pourquoi pas ici |
|---|---|---|
| CrowdSec | Listes communautaires partagées, scénarios, bouncers | Plus de pièces mobiles (agent, API locale, bouncer) et des données partagées avec un service externe. |
| sshguard | Petit, rapide, centré sur SSH | Couvre bien SSH, moins bien les jails HTTP. |
| Limites de débit dans l’ingress seulement | Protège HTTP sans démon supplémentaire | Ralentit les attaquants sans jamais les bannir, et ne fait rien pour SSH. Utilisé en complément (limite de la recherche). |
| fail2ban | Fonctionne avec tout journal, actions scriptables, mature | Choisi – avec les trois leçons ci-dessus intégrées. |
fail2ban est ancien, simple, et fait exactement ce qu’il promet. Le travail n’a jamais été l’outil ; il consistait à s’assurer que chaque « Ban » de son journal devenait aussi une entrée dans le noyau. Depuis octobre 2026, c’est vérifié – par le handler du pare-feu, par Monit et par une alerte sur les journaux – au lieu d’être supposé.


