Come funziona fail2ban
Un server esposto su Internet riceve di continuo tentativi di accesso automatici: password provate a raffica su SSH, sulle aree protette del sito, sui pannelli di login. fail2ban legge i log dei servizi, riconosce i tentativi falliti e, quando un indirizzo ne accumula troppi in poco tempo, lo blocca nel firewall per un periodo stabilito.
Il lavoro è organizzato in jail (“prigioni”). Ogni jail mette insieme quattro elementi:
- la sorgente dei log: un file o il journal di systemd;
- il filtro: le espressioni regolari (
failregex) che riconoscono un tentativo fallito ed estraggono l’indirizzo dell’attaccante; - le soglie: quanti tentativi (
maxretry) in quanto tempo (findtime) e per quanto bloccare (bantime); - l’azione: che cosa fare al ban, di solito aggiungere l’indirizzo a un set di nftables.
fail2ban non sostituisce il firewall e non rende sicura una password debole: riduce il rumore e rallenta gli attacchi automatici. Le basi restano un firewall con policy restrittiva (vedi Firewall completo con nftables) e SSH con le sole chiavi (vedi SSH sicuro su Linux).
Installazione e file di configurazione
sudo apt update
sudo apt install fail2ban
fail2ban-client version # Ubuntu 24.04: 1.0.2
sudo systemctl enable --now fail2ban
systemctl status fail2ban
Ubuntu 24.04 distribuisce fail2ban 1.0.2 con alcune modifiche del pacchetto Debian/Ubuntu che vedremo tra poco. Già dopo l’installazione la jail sshd è attiva.
La prima versione del pacchetto uscita con Ubuntu 24.04 non partiva, perché Python 3.12 non include più il modulo asynchat. Il problema è corretto negli aggiornamenti: se il servizio non si avvia con questo errore, esegui sudo apt update && sudo apt full-upgrade e controlla la versione con apt policy fail2ban.
I file di configurazione
La configurazione sta in /etc/fail2ban:
| Percorso | Contenuto |
|---|---|
jail.conf | Valori predefiniti e definizione di tutte le jail disponibili, quasi tutte disattivate |
jail.d/defaults-debian.conf | Impostazioni del pacchetto Ubuntu |
jail.local, jail.d/*.local | Le tue modifiche |
filter.d/ | Filtri: le espressioni regolari per ogni servizio |
action.d/ | Azioni: ban con nftables, iptables, invio di email… |
fail2ban.conf / fail2ban.local | Impostazioni del demone: log, database |
jail.conf appartiene al pacchetto: un aggiornamento può sovrascriverlo o chiederti di scegliere tra due versioni. Le tue impostazioni vanno nei file .local, che contengono solo le righe che cambiano. Non copiare jail.conf in jail.local: ti porteresti dietro centinaia di righe che non aggiornerai mai.
I file si leggono in quest’ordine, e ogni valore sovrascrive quello precedente: jail.conf, poi jail.d/*.conf in ordine alfabetico, poi jail.local, infine jail.d/*.local in ordine alfabetico. Ecco cosa contiene il file del pacchetto Ubuntu:
[DEFAULT]
banaction = nftables
banaction_allports = nftables[type=allports]
backend = systemd
[sshd]
enabled = trueDue scelte importanti: i ban si fanno con nftables (in jail.conf il valore predefinito sarebbe iptables) e i log si leggono dal journal di systemd, per tutte le jail. La seconda ha una conseguenza che spiego nella prossima sezione.
Journal o file di log: il backend
Il backend dice a fail2ban dove leggere:
systemd: legge dal journal, filtrando per servizio con le regolejournalmatchdel filtro. Il parametrologpathviene ignorato;auto(opyinotify,polling): legge i file indicati dalogpath.
Su Ubuntu 24.04 sshd scrive nel journal. Il file /var/log/auth.log esiste solo se è installato rsyslog, che copia i messaggi dal journal ai file classici: c’è nell’installazione standard di Ubuntu Server, ma manca spesso nelle immagini minimali, nei container e in alcune immagini cloud. Leggere dal journal funziona in tutti i casi, ed è per questo che il pacchetto imposta backend = systemd.
La conseguenza: il valore è in [DEFAULT], quindi vale anche per le jail che leggono file, come quelle di nginx e Apache o la jail recidive. Con il backend systemd quelle jail ignorano logpath e cercano nel journal messaggi che lì non ci sono: partono senza errori e non bannano mai nessuno. Per ogni jail che legge un file devi scrivere backend = auto, come vedrai negli esempi.
# i tentativi falliti di SSH nel journal
sudo journalctl -u ssh --since today | grep -E "Failed|Invalid"
# l'unità su Ubuntu si chiama ssh.service, non sshd.service
Non preoccuparti del nome dell’unità: il filtro sshd cerca nel journal sia per unità sia per nome del processo (_COMM=sshd), quindi trova i messaggi anche su Ubuntu.
Le impostazioni generali
Crea /etc/fail2ban/jail.local con i valori comuni a tutte le jail:
[DEFAULT]
# Indirizzi mai bannati: loopback e reti di amministrazione.
# Mettici SEMPRE la rete da cui ti colleghi.
ignoreip = 127.0.0.1/8 ::1 192.168.50.0/24 198.51.100.7 2001:db8:50::/64
# Soglie: 5 tentativi in 10 minuti = ban di 1 ora
maxretry = 5
findtime = 10m
bantime = 1h
# Ban progressivi: ogni nuovo ban dello stesso IP dura il doppio
# del precedente (1h, 2h, 4h...), fino a un massimo di una settimana
bantime.increment = true
bantime.maxtime = 1w
bantime.rndtime = 10m
# Azioni nftables (sono già le impostazioni del pacchetto Ubuntu:
# le ripeto per rendere esplicita la scelta)
banaction = nftables-multiport
banaction_allports = nftables-allportsignoreipaccetta indirizzi, reti CIDR e nomi DNS, separati da spazi. È la tua assicurazione contro il ban di te stesso.bantime.incrementconsulta il database di fail2ban per sapere quante volte un indirizzo è già stato bannato e allunga il ban di conseguenza.bantime.rndtimeaggiunge una parte casuale, così i bot non possono prevedere quando tornare.nftables-multiportblocca l’indirizzo solo sulle porte della jail;nftables-allportssu tutte. I file corrispondenti sono in/etc/fail2ban/action.d/:nftables.conf,nftables-multiport.conf,nftables-allports.conf.
Il database conserva la storia dei ban per un giorno, valore troppo basso per ban progressivi che arrivano a una settimana e per la jail recidive. Allungalo:
[DEFAULT]
dbpurgeage = 30dLa jail sshd
La jail è già attiva grazie al file del pacchetto. Personalizzala in un file dedicato:
[sshd]
enabled = true
# se sshd ascolta su un'altra porta: port = 2222
port = ssh
# aggressive: riconosce anche le connessioni chiuse prima
# dell'autenticazione e gli errori di negoziazione
filter = sshd[mode=aggressive]
backend = systemd
maxretry = 4La modalità aggressive unisce le modalità normal, ddos ed extra del filtro: utile su un server esposto, dove molti bot si limitano ad aprire e chiudere connessioni. Ricarica e verifica:
sudo fail2ban-client -t # controlla la configurazione
sudo systemctl reload fail2ban
sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 37
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 1
|- Total banned: 3
`- Banned IP list: 203.0.113.66
La riga Journal matches conferma che la jail legge dal journal. Se invece vedi File list: /var/log/auth.log, sta usando il file.
Le jail per nginx e Apache
Le jail nginx-http-auth e apache-auth riconoscono gli errori di autenticazione HTTP Basic (le aree protette da auth_basic o AuthType Basic). Leggono i log degli errori, che sono file: ricordati backend = auto.
[nginx-http-auth]
enabled = true
backend = auto
port = http,https
logpath = /var/log/nginx/error.log
# Se usi Apache invece di nginx:
# [apache-auth]
# enabled = true
# backend = auto
# port = http,https
# logpath = /var/log/apache2/error.logIn jail.conf esistono altre jail pronte per il web, per esempio nginx-limit-req (richieste respinte dal limite limit_req di nginx) e nginx-botsearch (scansioni alla ricerca di pagine vulnerabili). Si attivano allo stesso modo.
Se davanti al server c’è un bilanciatore o una CDN, nel log compare l’indirizzo del proxy, non quello del client. fail2ban bannerebbe il proxy, cioè tutti. Configura prima il server web perché registri l’IP reale (modulo real_ip di nginx, mod_remoteip di Apache); anche così, il ban nel firewall locale non ferma il traffico che arriva dal proxy, e va fatto sul proxy stesso.
La jail recidive
Alcuni bot aspettano la fine del ban e ricominciano. La jail recidive legge il log di fail2ban stesso e banna più a lungo, su tutte le porte, chi viene bannato ripetutamente da qualsiasi jail.
[recidive]
enabled = true
backend = auto
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
# 3 ban in un giorno = fuori per una settimana
maxretry = 3
findtime = 1d
bantime = 1wCon i ban progressivi già attivi, recidive è in parte ridondante, ma ha un vantaggio: blocca l’indirizzo su tutte le porte, non solo su quella del servizio attaccato. Il findtime di un giorno funziona solo se dbpurgeage è almeno altrettanto lungo, come abbiamo impostato prima.
Un filtro personalizzato
Esempio pratico: un sito WordPress dietro nginx. I tentativi di login falliti su /wp-login.php rispondono con codice 200 (la pagina di login viene ripresentata), quelli riusciti con un redirect 302. Nel log di accesso di nginx una riga tipica è:
203.0.113.66 - - [12/Nov/2026:10:15:42 +0100] "POST /wp-login.php HTTP/1.1" 200 4521 "-" "Mozilla/5.0"
Crea il filtro:
[Definition]
# <HOST> è il segnaposto che cattura l'indirizzo IPv4 o IPv6.
# fail2ban riconosce e toglie da solo la data, quindi il filtro
# non deve descriverla
failregex = ^<HOST> -[^"]*"POST /wp-login\.php[^"]*" 200
ignoreregex =Prima di attivarlo, provalo sul log reale con fail2ban-regex:
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-wp-login.conf
Nel riepilogo guarda le righe Failregex: N total e Lines: … matched: devono corrispondere al numero di tentativi falliti che ti aspetti. Con --print-all-matched vedi le righe riconosciute, con --print-all-missed quelle scartate. Lo stesso comando funziona sul journal, per esempio per provare il filtro di SSH:
sudo fail2ban-regex systemd-journal sshd
Quando il filtro è corretto, crea la jail:
[nginx-wp-login]
enabled = true
backend = auto
filter = nginx-wp-login
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 5msudo fail2ban-client -t && sudo systemctl reload fail2ban
sudo fail2ban-client status nginx-wp-login
Due consigli sulle espressioni: ancorale sempre all’inizio della riga con ^, e non usare .* prima di <HOST>. Un’espressione troppo larga può catturare un indirizzo scritto dall’attaccante in un altro campo (per esempio nello user agent) e farti bannare chiunque.
Gestire i ban e vedere le regole
I comandi di fail2ban-client
sudo fail2ban-client status # elenco delle jail attive
sudo fail2ban-client status sshd # dettaglio e IP bannati
sudo fail2ban-client set sshd banip 203.0.113.66 # ban manuale
sudo fail2ban-client set sshd unbanip 203.0.113.66 # sblocco
sudo fail2ban-client unban 203.0.113.66 # sblocco da tutte le jail
sudo fail2ban-client reload sshd # ricarica una sola jail
sudo fail2ban-client -t # verifica la configurazione
Il log del demone racconta tutto quello che succede: Found per ogni tentativo riconosciuto, Ban e Unban.
sudo tail -f /var/log/fail2ban.log
sudo grep " Ban " /var/log/fail2ban.log | tail
I ban in nftables
Le azioni nftables creano una tabella dedicata, inet f2b-table, con una catena agganciata a input a priorità −1 (quindi valutata prima di un firewall a priorità filter) e un set di indirizzi per ogni jail: addr-set-sshd per IPv4, addr6-set-sshd per IPv6.
sudo nft list table inet f2b-table
sudo nft list set inet f2b-table addr-set-sshd
table inet f2b-table {
set addr-set-sshd {
type ipv4_addr
elements = { 203.0.113.66 }
}
chain f2b-chain {
type filter hook input priority filter - 1; policy accept;
tcp dport 22 ip saddr @addr-set-sshd reject
}
}
L’output reale contiene anche i set IPv6 e le regole delle altre jail. Il blocco predefinito è reject: l’attaccante riceve un rifiuto immediato. Le scadenze non sono nel set: è fail2ban a togliere l’indirizzo allo scadere del bantime, anche dopo un riavvio, grazie al suo database.
Se il tuo /etc/nftables.conf inizia con flush ruleset, ogni ricarica del firewall elimina anche la tabella f2b-table. Dopo systemctl reload nftables esegui sempre sudo systemctl restart fail2ban, che ricrea tabella e ban. I dettagli nella guida Firewall completo con nftables.
Notifica via email (facoltativa)
fail2ban può mandarti un messaggio a ogni ban. Serve un programma sendmail funzionante sul server, per esempio Postfix configurato come relay o il più leggero msmtp-mta, e il pacchetto whois per le informazioni sull’indirizzo bannato. Aggiungi a [DEFAULT] in jail.local:
destemail = sistemi@example.com
sender = fail2ban@server1.example.com
mta = sendmail
# ban + email con whois; action_mwl aggiunge anche le righe di log
action = %(action_mw)sSu un server molto attaccato riceveresti decine di messaggi al giorno: valuta di attivare l’email solo per la jail recidive, scrivendo action = %(action_mw)s nella sua sezione invece che in [DEFAULT].
Problemi comuni
| Sintomo | Causa probabile | Cosa fare |
|---|---|---|
| Il servizio fail2ban non parte | Errore di sintassi in un file .local, pacchetto non aggiornato (asynchat) | sudo fail2ban-client -t e journalctl -u fail2ban; aggiorna il pacchetto |
Una jail non compare in fail2ban-client status | Manca enabled = true, nome della sezione sbagliato, file con estensione diversa da .conf/.local | Controlla il nome del file e della sezione; leggi /var/log/fail2ban.log all’avvio |
| “Have not found any log file” all’avvio | logpath inesistente, per esempio /var/log/auth.log senza rsyslog | Per SSH usa backend = systemd; per i file verifica il percorso |
La jail web è attiva ma Total failed resta a 0 | Ereditato backend = systemd dal file del pacchetto: logpath viene ignorato | Aggiungi backend = auto nella jail; verifica che status mostri File list |
| Il filtro non trova niente | Formato del log diverso da quello previsto dal filtro | Prova con fail2ban-regex e --print-all-missed |
| L’IP risulta bannato ma continua a collegarsi | Porta della jail diversa da quella reale, tabella f2b-table cancellata da una ricarica di nftables, traffico verso container Docker (passa da forward, non da input) | Controlla port; sudo nft list table inet f2b-table; riavvia fail2ban |
| Viene bannato sempre lo stesso indirizzo interno | Proxy, bilanciatore o NAT davanti al server | Fai registrare al server web l’IP reale del client; aggiungi il proxy a ignoreip |
| Ti sei bannato da solo | Troppi tentativi da un indirizzo non presente in ignoreip | Da un altro indirizzo o dalla console: sudo fail2ban-client set sshd unbanip <tuo-IP>; poi aggiungi la tua rete a ignoreip |
| I ban progressivi non crescono | dbpurgeage troppo basso: la storia dei ban viene cancellata | Imposta dbpurgeage in fail2ban.local almeno pari a bantime.maxtime |
Il passo successivo
fail2ban è l’ultimo strato, non il primo. Riduci prima la superficie d’attacco: un firewall con policy drop e SSH aperto solo alle reti di amministrazione (Firewall completo con nftables), poi SSH con le sole chiavi e senza accesso di root (SSH sicuro su Linux). Con questi due passi, nei log di fail2ban vedrai molti meno tentativi, e quelli rimasti meritano davvero attenzione.
Prova sempre la configurazione in laboratorio, salva quella attuale prima di modificarla e tieni un accesso di emergenza (console o accesso fuori banda) quando lavori su firewall e VPN da remoto.