fail2ban est l’un des outils les plus anciens et les plus répandus de la boîte à outils de l’administrateur : il lit les journaux, compte les échecs d’authentification et bannit au pare-feu les adresses trop insistantes. Simple sur le papier, il est pourtant souvent mal réglé : bannissements de dix minutes qui n’arrêtent personne, réglages écrits dans un fichier écrasé à la mise à jour, jail SSH qui ne voit aucune ligne parce qu’elle lit le mauvais journal, ou administrateur banni de son propre serveur.

Ce tutoriel part de la configuration réelle de mes serveurs Rocky Linux : bannissements progressifs, quatre niveaux de récidive, une jail pour une application maison, et les vérifications qui permettent de savoir que tout cela fonctionne vraiment.

Échelle des bannissements fail2ban de 1 h à 30 jours, avec la prison recidive et les réglages importants
Un bannissement qui grandit à chaque récidive, plafonné à 30 jours.

Trois notions

NotionCe que c’estOù
filtreles expressions régulières qui reconnaissent un échec dans un journalfilter.d/*.conf
actionce qui est fait pour bannir et débannir (règle firewalld, nftables, courriel…)action.d/*.conf
jaill’association d’un filtre, d’une source de journaux, d’une action et de seuilsjail.local, jail.d/*.local

fail2ban fournit des dizaines de filtres et d’actions prêts à l’emploi. Le travail consiste surtout à écrire les jails : quoi surveiller, avec quels seuils, et comment bannir.

Étape 1 : installer, et ne jamais toucher jail.conf

dnf install epel-release
dnf install fail2ban fail2ban-firewalld
systemctl enable --now fail2ban

Le paquet fail2ban-firewalld installe un fichier qui fait de firewalld l’action par défaut. Les fichiers jail.conf et fail2ban.conf appartiennent au paquet et seront remplacés à la prochaine mise à jour : tous vos réglages vont dans les fichiers .local, qui les surchargent clé par clé. Un jail.local peut donc ne contenir que les quelques lignes qui diffèrent.

Étape 2 : le service lui-même

# /etc/fail2ban/fail2ban.local
[DEFINITION]
logtarget  = /var/log/fail2ban/fail2ban.log
dbfile     = /var/lib/fail2ban/fail2ban.sqlite3
dbpurgeage = 35d
  • logtarget : un fichier dédié plutôt que le journal système. Il servira de source à la jail de récidive, et il est plus facile à lire ;
  • dbfile : la base SQLite où fail2ban garde les bannissements en cours et leur historique. C’est elle qui permet de rétablir les bannissements après un redémarrage, et de savoir combien de fois une adresse a déjà été bannie ;
  • dbpurgeage : la durée de conservation de l’historique. Par défaut, un jour seulement : trop court pour que les bannissements progressifs et la récidive à six mois aient une mémoire. 35 jours est un compromis ; la récidive longue s’appuie de toute façon sur le fichier journal.

Créez le répertoire et le fichier avant de redémarrer : install -d /var/log/fail2ban && touch /var/log/fail2ban/fail2ban.log. Pensez aussi à adapter la configuration logrotate du paquet à ce nouveau chemin.

Étape 3 : jail.local, ligne par ligne

# /etc/fail2ban/jail.local
[DEFAULT]
ignoreip  = 127.0.0.1/8 ::1 203.0.113.25 203.0.113.40 198.51.100.10
bantime   = 1h
findtime  = 10m
maxretry  = 5
bantime.increment = true
bantime.formula   = ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor
bantime.maxtime   = 720h
banaction          = firewallcmd-rich-rules
banaction_allports = firewallcmd-rich-rules

[sshd]
enabled  = true
backend  = systemd
maxretry = 5
bantime  = 2h

Ne pas se bannir soi-même

ignoreip liste les adresses jamais bannies : la boucle locale, l’adresse publique de chacun de vos serveurs, et celle de votre accès d’administration. Sans elle, quelques tests de connexion ratés suffisent à vous enfermer dehors, et un serveur qui interroge un autre serveur avec un mauvais mot de passe le fera bannir. Mettez la même liste partout.

Les seuils

findtime et maxretry : 5 échecs en 10 minutes déclenchent un bannissement. Plus bas, on bannit des utilisateurs qui se trompent de mot de passe ; plus haut, on laisse aux robots plus d’essais. Pour SSH, si seule l’authentification par clé est autorisée, un utilisateur légitime n’échoue presque jamais et on peut descendre à 3.

Les bannissements progressifs

bantime.increment active l’allongement automatique. La formule multiplie la durée de base par 2 à la puissance du nombre de bannissements précédents de cette adresse (1<<n vaut 2n), plafonné à 220 pour éviter les dépassements, puis par banFactor (1 par défaut). bantime.maxtime limite le résultat à 30 jours :

Bannissement n°Durée (bantime 1 h)Durée (jail sshd, 2 h)
11 h2 h
22 h4 h
34 h8 h
516 h32 h
8128 h (5 j)256 h (10 j)
10 et plusplafond : 30 jplafond : 30 j

L’effet est exactement celui qu’on veut : une erreur ponctuelle coûte une heure, un robot qui revient tous les jours se retrouve banni pour des semaines au bout d’une dizaine de passages.

L’action

banaction = firewallcmd-rich-rules : chaque bannissement devient une règle riche firewalld qui rejette l’adresse sur le port de la jail. banaction_allports est l’action utilisée par les jails qui bannissent sur tous les ports (la récidive). Sur un serveur sans firewalld, utilisez nftables-multiport et nftables-allports.

La jail SSH

backend = systemd est indispensable sur Rocky Linux : sshd écrit dans le journal systemd, et une jail qui lit un fichier ne verrait rien, sans aucune erreur. La jail lit les entrées de l’unité sshd.service. Une seconde subtilité : les versions récentes d’OpenSSH écrivent une partie de leurs messages sous le nom sshd-session. Si fail2ban-client status sshd reste à zéro alors que des échecs apparaissent dans journalctl, vérifiez que le filtre de votre version de fail2ban connaît ce nom (journalmatch).

Étape 4 : punir les récidivistes

La jail recidive lit le propre journal de fail2ban et compte les lignes « Ban ». Une adresse bannie plusieurs fois, quelle que soit la jail, est alors bannie sur tous les ports, pour plus longtemps. Quatre paliers complètent bien les bannissements progressifs :

[recidive1]
enabled   = true
filter    = recidive
logpath   = /var/log/fail2ban/fail2ban.log
banaction = %(banaction_allports)s
findtime  = 1d
maxretry  = 4
bantime   = 3h

[recidive2]
enabled   = true
filter    = recidive
logpath   = /var/log/fail2ban/fail2ban.log
banaction = %(banaction_allports)s
findtime  = 1w
maxretry  = 7
bantime   = 1d

[recidive3]
enabled   = true
filter    = recidive
logpath   = /var/log/fail2ban/fail2ban.log
banaction = %(banaction_allports)s
findtime  = 30d
maxretry  = 10
bantime   = 1w

[recidive4]
enabled   = true
filter    = recidive
logpath   = /var/log/fail2ban/fail2ban.log
banaction = %(banaction_allports)s
findtime  = 180d
maxretry  = 20
bantime   = 30d

Chaque palier a sa propre fenêtre : une adresse bannie 4 fois en une journée l’est pour 3 heures sur tous les ports ; 20 fois en six mois, pour un mois. Les fenêtres longues imposent de garder le journal assez longtemps : réglez la rotation pour conserver au moins six mois, compressés. La jail refuse de démarrer si le fichier journal n’existe pas encore, d’où sa création à l’étape 2.

Étape 5 : une jail pour sa propre application

Pour protéger une application qui n’a pas de filtre fourni, écrivez-en un. Prenons une application qui journalise en JSON :

# /etc/fail2ban/filter.d/monappli.conf
[Definition]
failregex = ^.*"msg":"login failed".*"remote_ip":"<HOST>".*$
ignoreregex =
datepattern = "time":"%%Y-%%m-%%dT%%H:%%M:%%S
  • <HOST> : la partie qui capture l’adresse à bannir ; fail2ban la remplace par une expression qui reconnaît IPv4 et IPv6 ;
  • datepattern : comment lire la date de la ligne. fail2ban ignore les lignes plus vieilles que findtime, il doit donc comprendre la date. Les %% sont des % échappés pour le format du fichier ;
  • ignoreregex : des lignes à ne jamais compter, même si elles correspondent.
# /etc/fail2ban/jail.d/monappli.local
[monappli]
enabled  = true
filter   = monappli
logpath  = /var/log/monappli/app.log
port     = 2022,8443
maxretry = 5

port limite le bannissement aux ports de l’application : l’adresse fautive garde l’accès au reste, à moins de récidiver.

Étape 6 : tester un filtre avant de l’activer

fail2ban-regex /var/log/monappli/app.log /etc/fail2ban/filter.d/monappli.conf
#   Failregex: 12 total
#   Lines: 4821 lines, 0 ignored, 12 matched, 4809 missed
fail2ban-regex --print-all-matched /var/log/monappli/app.log /etc/fail2ban/filter.d/monappli.conf

Un filtre qui ne trouve rien (« 0 matched ») sur un journal contenant des échecs connus est faux ; un filtre qui trouve trop (des connexions réussies) bannira vos utilisateurs. Vérifiez aussi la ligne « Date template hits » : si la date n’est pas reconnue, aucune ligne ne sera jamais comptée en production. Pour SSH avec le backend systemd : fail2ban-regex systemd-journal sshd.

Étape 7 : vérifier et exploiter

fail2ban-client -t                         # la configuration est-elle valide ?
systemctl restart fail2ban
fail2ban-client status                     # les jails actives
fail2ban-client status sshd                # échecs comptés, adresses bannies
fail2ban-client get sshd banip --with-time # avec la fin de chaque bannissement
fail2ban-client set sshd unbanip 192.0.2.66
fail2ban-client unban --all                # en cas d'urgence
firewall-cmd --list-rich-rules | grep -c reject

Faites le test pour de vrai, depuis une adresse qui n’est pas dans ignoreip (une connexion mobile, par exemple) : cinq mots de passe faux, puis vérifiez que la sixième tentative est refusée et que l’adresse apparaît dans le statut de la jail.

À quoi ça ressemble après quelques mois

Mon serveur web le 2 novembre 2026 (fail2ban 1.1.0). La jail SSH lit le journal systemd, et quatre jails de récidive s’empilent derrière elle :

# fail2ban-client status
|- Number of jail:	5
`- Jail list:	recidive1, recidive2, recidive3, recidive4, sshd

# fail2ban-client status sshd
|- Filter
|  |- Currently failed:	0
|  |- Total failed:	6
|  `- Journal matches:	_SYSTEMD_UNIT=sshd.service + _COMM=sshd + _COMM=sshd-session
`- Actions
   |- Currently banned:	2
   |- Total banned:	2

# for j in recidive1 recidive2 recidive3 recidive4; do ...; done
recidive1: bantime=10800   findtime=86400    maxretry=4
recidive2: bantime=86400   findtime=604800   maxretry=7
recidive3: bantime=604800  findtime=2592000  maxretry=10
recidive4: bantime=2592000 findtime=15552000 maxretry=20
  • Total failed: 6 depuis le redémarrage du matin. Les connexions par mot de passe étant désactivées, presque personne n’arrive même jusqu’à un échec : la plupart des robots abandonnent dès l’échange de clés. Les compteurs repartent de zéro avec le service ; l’historique est dans la base.
  • Journal matches cite à la fois sshd et sshd-session : depuis OpenSSH 9.8, les messages d’authentification viennent du processus sshd-session. Un filtre qui ne regarde que _COMM=sshd ne compte plus rien, sans le dire, sur un système récent.
  • Les quatre jails de récidive lisent le journal de fail2ban lui-même : 4 bannissements en un jour donnent 3 heures, 7 en une semaine une journée, 10 en un mois une semaine, 20 en six mois un mois.

Les bannissements progressifs se lisent dans la base SQLite de fail2ban, qui garde chaque adresse avec son nombre de bannissements :

# sqlite3 -readonly /var/lib/fail2ban/fail2ban.sqlite3 \
    "select jail, bancount, count(*) from bips group by jail, bancount"
recidive3|1|1
sshd|1|21
sshd|3|1
sshd|7|1
sshd|10|1

# sqlite3 -readonly /var/lib/fail2ban/fail2ban.sqlite3 "select max(bantime) from bips where jail='sshd'"
2592000

Vingt et une adresses ont été bannies une fois et ne sont jamais revenues : c’est l’essentiel du trafic, des robots opportunistes. Trois sont revenues, dont une dix fois. Pour celle-là, chaque bannissement a doublé la durée du précédent jusqu’à atteindre bantime.maxtime = 720h : 2 592 000 secondes, trente jours, le plafond. C’est tout l’intérêt de la progression : la première faute coûte peu, les obstinés finissent exclus un mois, sans liste manuelle à entretenir.

Les pannes fréquentes

La jail ne compte rien

Mauvais backend (fichier au lieu de systemd), mauvais chemin de journal, ou date non reconnue. fail2ban-client status <jail> montre la source lue ; fail2ban-regex dit si les lignes correspondent.

Les adresses sont bannies mais continuent de passer

L’action ne correspond pas au pare-feu réellement utilisé (une action iptables sur un système nftables pur, par exemple), ou le trafic arrive par un mandataire inverse et c’est l’adresse du mandataire qui est bannie. Derrière un mandataire, l’application doit journaliser l’adresse réelle du client (X-Forwarded-For), et le bannissement doit avoir lieu sur la machine qui reçoit la connexion.

Une adresse bloquée alors que fail2ban ne la bannit pas

Si CrowdSec tourne aussi, il a peut-être pris sa propre décision : cscli decisions list --ip …. Les deux se complètent bien (fail2ban réagit à vos propres journaux, CrowdSec ajoute des scénarios plus riches et une liste communautaire), mais il faut penser à regarder des deux côtés.

fail2ban met longtemps à démarrer

À chaque démarrage, il rétablit les bannissements en cours depuis sa base et relit la fin des journaux. Avec des milliers d’entrées et des règles riches firewalld, cela peut prendre une minute. Une action nftables par ensemble (nftables-allports) est bien plus rapide quand le nombre de bannissements grossit.


Ce billet fait partie d’une série de tutoriels d’administration système (tag « pas à pas »).
Série « Pas à pas », tutoriel 11/77. · ← Tutoriel précédent · Tutoriel suivant → (à paraître le 16/11) · English version

Articles liés