Configurare fail2ban e bloccare automaticamente gli attacchi brute force

Pubblicato il 15 min di lettura

Come configurare fail2ban in modo pulito tramite jail.local, far funzionare il jail sshd su tutte e quattro le distribuzioni LTS attuali, verificare i blocchi e toglierli di nuovo, e quali errori altrimenti ti rallentano.

Ogni server con un indirizzo IPv4 pubblico riceve tentativi di login sulla porta 22 già pochi minuti dopo la prima installazione. Non è un attacco mirato, è rumore di fondo. fail2ban legge le righe di log di questi tentativi falliti e fa bloccare al firewall l'IP di origine per un tempo definito. Lo strumento è pacchettizzato in tutte le distribuzioni trattate qui e si mette in piedi in pochi minuti. Le insidie stanno altrove: su Debian 12 fail2ban parte volentieri con un errore di configurazione, su certi sistemi sembra bloccare senza che venga scartato davvero un solo pacchetto, e una riga sconsiderata in jail.local chiude fuori proprio te.

Questo articolo copre esattamente questi punti, per Debian 13, Debian 12, Ubuntu 24.04 LTS e Ubuntu 22.04 LTS.

Perché jail.local e mai jail.conf

Il file /etc/fail2ban/jail.conf appartiene al pacchetto. A ogni aggiornamento può essere sovrascritto, e nel migliore dei casi dpkg ti chiede come procedere, nel peggiore (durante un aggiornamento non presidiato) non chiede proprio niente. Le tue personalizzazioni vivono perciò in /etc/fail2ban/jail.local. Questo file dopo l'installazione non esiste, lo crei tu, e deve contenere solo i valori che cambi davvero.

Conta l'ordine in cui fail2ban legge. La pagina di manuale jail.conf(5) lo indica esplicitamente:

  1. jail.conf
  2. jail.d/*.conf in ordine alfabetico
  3. jail.local
  4. jail.d/*.local in ordine alfabetico

I file letti più tardi vincono. In concreto significa: la tua jail.local sovrascrive anche le impostazioni di /etc/fail2ban/jail.d/defaults-debian.conf, il file che porta con sé il pacchetto della distribuzione. Molte guide affermano il contrario e per questo consigliano un file proprio in jail.d/. Non è necessario. jail.d/ ha senso solo se vuoi distribuire i singoli jail uno per uno tramite un sistema di gestione della configurazione.

Che cosa impone già la tua distribuzione

La differenza più grande tra i quattro sistemi non sta in fail2ban stesso, ma in quell'unico file che il pacchetto deposita in /etc/fail2ban/jail.d/defaults-debian.conf. Guardalo per primo:

cat /etc/fail2ban/jail.d/defaults-debian.conf
Sistemafail2banContenuto di defaults-debian.conf
Debian 131.1.0banaction = nftables, banaction_allports = nftables[type=allports], per [sshd] in più backend = systemd, journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd ed enabled = true
Debian 121.0.2solo [sshd] ed enabled = true
Ubuntu 24.041.0.2banaction = nftables, banaction_allports = nftables[type=allports], backend = systemd, più [sshd] con enabled = true
Ubuntu 22.040.11.2solo [sshd] ed enabled = true

Da qui discende quasi tutto il resto. Debian 13 e Ubuntu 24.04 leggono di serie il journal di systemd e bloccano tramite nftables. Debian 12 e Ubuntu 22.04 ricadono sulle impostazioni di jail.conf, quindi banaction = iptables-multiport e backend = auto. E auto non significa affatto "scegli tu quello giusto". Il commento in jail.conf dice che auto prova nell'ordine pyinotify e polling. Entrambi sono metodi basati su file. Il valore auto non sceglie mai il journal. Senza il file /var/log/auth.log il jail sshd gira lì a vuoto.

Installazione e primo controllo

apt update
apt install -y fail2ban

Su Debian 12 e Ubuntu 22.04 serve un pacchetto in più, se vuoi analizzare il journal invece di un file di log:

apt install -y python3-systemd

Su Debian 13 python3-systemd è una dipendenza rigida del pacchetto ed è già presente. Su Debian 12 è soltanto raccomandato, ed è la fonte di errore più frequente su questo sistema. Verifica poi versione e stato:

apt-cache policy fail2ban
fail2ban-client --version
systemctl status fail2ban --no-pager

Se qui compare già active (running), il grosso è fatto. Se invece c'è scritto failed, salta direttamente alla sezione sui messaggi di errore.

Costruire la propria jail.local

Crea /etc/fail2ban/jail.local. Questa versione funziona su tutti e quattro i sistemi, a patto che python3-systemd sia installato:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.7
bantime  = 1h
findtime = 10m
maxretry = 5

bantime.increment = true
bantime.factor    = 2
bantime.maxtime   = 7d

banaction = nftables
banaction_allports = nftables[type=allports]

[sshd]
enabled  = true
backend  = systemd
port     = ssh
maxretry = 4
bantime  = 2h

Il significato dei valori in chiaro: findtime è la finestra temporale mobile, maxretry il numero di tentativi falliti al suo interno, bantime la durata del blocco. Quattro tentativi falliti in dieci minuti portano qui a due ore di ban. Le indicazioni di tempo accettano suffissi come m, h, d e w, un numero puro significa secondi. Un bantime di -1 blocca in modo permanente.

La parte interessante è bantime.increment. Con questa opzione fail2ban raddoppia il tempo di blocco a ogni ricaduta dello stesso IP, con il tetto fissato da bantime.maxtime. Un singolo errore di battitura di un utente reale costa due ore, un bot ostinato dopo pochi giri arriva a una settimana. La funzione esiste da fail2ban 0.11, quindi è disponibile anche su Ubuntu 22.04. Perché resti valida anche attraverso i riavvii, il database deve conservare lo storico, cosa che si regola con dbpurgeage in /etc/fail2ban/fail2ban.conf, di serie un giorno. Se imposti bantime.maxtime = 7d, alza di conseguenza anche dbpurgeage in un file fail2ban.local.

Due trappole in questo blocco. Primo, port = ssh: si risolve tramite /etc/services nella porta 22. Se il tuo servizio SSH gira sulla 2222, lì deve esserci port = 2222, altrimenti nasce una regola firewall per una porta a cui non bussa nessuno. Secondo, ignoreip: inserisci lì l'IP fisso del tuo ufficio, ma mai un'intera rete di un provider. E non affidarti solo a quello. Il valore ignoreself è comunque impostato di serie su true e protegge gli IP del server stesso.

Differenze a seconda del sistema

Su Debian 12 hai due strade equivalenti. O resti con backend = systemd e installi python3-systemd, oppure installi rsyslog, ometti backend e lavori con /var/log/auth.log. Su Debian 12 dovresti inoltre impostare banaction in modo consapevole. Il pacchetto raccomanda nftables oppure iptables, e di questi apt di norma installa solo nftables. L'impostazione predefinita di jail.conf è però iptables-multiport. Su un'installazione snella fail2ban ricorre quindi a un binario che non c'è proprio.

Su Ubuntu 22.04 rsyslog è incluso nello standard server, /var/log/auth.log esiste e il jail funziona senza interventi. Se lì non vuoi cambiare niente, ometti semplicemente backend e banaction nella tua jail.local. fail2ban 0.11.2 conosce sì l'azione nftables, ma su questo sistema la via di iptables-multiport è quella più battuta.

Su Debian 13 e Ubuntu 24.04 la tua jail.local coincide con le impostazioni del pacchetto. Non fa danni, rende soltanto la configurazione esplicita e quindi leggibile.

Applicare e verificare che funzioni davvero

Rileggere la configurazione senza perdere i ban già attivi:

fail2ban-client reload

Su Debian 12 e Ubuntu 24.04 fail2ban 1.0.2 segnala all'avvio e al ricaricamento l'avviso 'allowipv6' not defined in 'Definition'. È innocuo, il servizio continua comunque a lavorare normalmente.

Un systemctl restart fail2ban serve di rado e azzera i ban in corso. Poi le interrogazioni di stato:

fail2ban-client ping
fail2ban-client status
fail2ban-client status sshd

L'ultimo output è quello significativo. Mostra Currently failed, Total failed, il numero dei ban e l'elenco degli indirizzi bloccati. È esattamente qui che la maggior parte delle guide si ferma, ed è esattamente qui che comincia il problema: un jail che parte pulito e riporta "0 banned" può essere completamente cieco. Un jail senza riscontri ha lo stesso identico aspetto di un jail che legge la sorgente di log sbagliata.

Tre controlli rendono visibile questa differenza. Primo: il filtro vede davvero quelle righe?

fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf

Con un backend basato su file, invece:

fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

Alla fine compare una riga come Lines: 4211 lines, 0 ignored, 137 matched, 4074 missed. Se lì c'è 0 matched mentre nel log si vedono chiaramente dei tentativi falliti, non torna il filtro oppure la selezione nel journal.

Secondo: quale filtro sul journal usa realmente il jail?

fail2ban-client get sshd journalmatch

La risposta cambia a seconda del sistema, e questa differenza è importante. Debian 13 riporta _SYSTEMD_UNIT=ssh.service + _COMM=sshd, Ubuntu 24.04 invece _SYSTEMD_UNIT=sshd.service + _COMM=sshd. Lì la unit si chiama quindi sshd.service, su Debian ssh.service. Su Debian 12 e Ubuntu 22.04 allo stato di fabbrica torna No journal match filter set, perché lì il backend systemd non è preimpostato e il jail legge un file di log. Non è un errore, è la conferma che un filtro sul journal non esiste proprio. Solo con backend = systemd nella tua jail.local su questi sistemi ne diventa attivo uno.

Le singole condizioni sono separate da + e vengono combinate in OR. _SYSTEMD_UNIT=ssh.service + _COMM=sshd corrisponde quindi a tutto ciò che proviene dalla unit ssh.service oppure a qualsiasi processo di nome sshd. Per la controprova nel journal usa esattamente il nome della unit che compare nell'output qui sopra:

journalctl -u ssh --no-pager -n 50
journalctl _COMM=sshd --no-pager -n 50

Su Ubuntu 24.04 la prima riga diventa di conseguenza journalctl -u sshd --no-pager -n 50. Chi usa qui il nome sbagliato vede un output vuoto e poi cerca dalla parte sbagliata.

Se qui compaiono righe con Failed password for invalid user ma fail2ban non conta niente, la causa sta nella selezione nel journal. Diventa rilevante quando usi OpenSSH dai backports oppure passi all'attivazione via socket. Da OpenSSH 9.8 il processo figlio si chiama sshd-session e non più sshd, e con l'attivazione via socket la unit non si chiama più ssh.service. In quel caso aggiungi nella tua jail.local (su Ubuntu 24.04 con _SYSTEMD_UNIT=sshd.service):

[sshd]
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd + _COMM=sshd-session

Terzo, la prova dei fatti. Blocca a mano un indirizzo della rete di documentazione e controlla se arriva davvero nel firewall:

fail2ban-client set sshd banip 203.0.113.10

Il comando risponde con 1, cioè un indirizzo bloccato. Con quale strumento verifichi questo blocco dipende dalla banaction effettiva, che a seconda della distribuzione è preimpostata in modo diverso:

Sistemabanaction di serieVerifica del blocco attivo
Debian 13nftablesnft list table inet f2b-table
Ubuntu 24.04nftablesnft list table inet f2b-table
Debian 12iptables-multiportiptables -n -L f2b-sshd
Ubuntu 22.04iptables-multiportiptables -n -L f2b-sshd

Il più vecchio Debian 11 si comporta come Debian 12. Da dove venga la differenza lo vedi in /etc/fail2ban/jail.d/defaults-debian.conf: solo Debian 13 e Ubuntu 24.04 portano lì un banaction = nftables, gli altri restano con l'impostazione iptables-multiport di jail.conf. Chi non vuole tenere a mente questa differenza server per server mette banaction = nftables nella propria jail.local, come nell'esempio più sopra, e poi verifica ovunque con nft.

Con l'azione nftables esiste una tabella f2b-table nella famiglia inet, al suo interno una chain f2b-chain con priorità filter - 1 e un set di nome addr-set-sshd. Lì deve comparire 203.0.113.10 come elemento. Una catena f2b-sshd su questi sistemi non esiste, e iptables -n -L f2b-sshd risponde infatti con No chain/target/match by that name. Al contrario, su Debian 12 e Ubuntu 22.04 nft list table inet f2b-table segnala Error: No such file or directory, finché lì lavora iptables-multiport. Nessuno dei due messaggi dimostra una configurazione rotta, dimostrano solo che il comando non corrisponde all'azione impostata.

Con l'azione iptables 203.0.113.10 deve comparire di conseguenza nella catena f2b-sshd. Solo quando l'IP compare nel firewall la catena è chiusa dal log fino al pacchetto scartato. Non dimenticare di ripulire.

Togliere di nuovo i blocchi

Sbloccare un singolo indirizzo da un jail preciso:

fail2ban-client set sshd unbanip 203.0.113.10

Un indirizzo da tutti i jail in una volta sola:

fail2ban-client unban 203.0.113.10

E in caso di emergenza, tutto a zero:

fail2ban-client unban --all

L'ultimo comando è quello che ti serve quando ti sei chiuso fuori da solo e sei seduto davanti a una console di emergenza sul server. Toglie tutti i blocchi senza riavviare il servizio. Se al server non arrivi proprio più, resta solo la strada della console nell'area clienti. Sui server root KVM e sui server dedicati di KernelHost trovi lì una sessione VNC che funziona indipendentemente da SSH. È anche il motivo per cui su un server senza una seconda via di accesso non dovresti impostare bantime = -1.

Un blocco che togli torna subito se i tentativi falliti vengono ancora conteggiati nella finestra temporale. Per questo unban azzera anche i relativi tentativi falliti. Chi deve restare libero in modo permanente va in ignoreip, seguito da fail2ban-client reload.

Convivenza con UFW e nftables

Qui nasce il danno più difficile da notare, perché fail2ban continua allegramente a segnalare blocchi mentre i pacchetti passano.

UFW è un frontend per iptables. All'avvio o al ricaricamento riscrive la tabella filter tramite iptables-restore. In quel momento le chain create da fail2ban spariscono senza sostituzione, e fail2ban non le ricrea da solo. Dopo ogni ufw enable, ufw disable o ufw reload vale perciò la regola: systemctl restart fail2ban. Fino ad allora in fail2ban-client status sshd compare un lungo elenco di IP bloccati, di cui nemmeno uno viene davvero fermato.

È più pulito far bloccare fail2ban direttamente tramite UFW. L'azione si trova in /etc/fail2ban/action.d/ufw.conf ed è presente in tutte e quattro le distribuzioni. In jail.local:

[DEFAULT]
banaction = ufw
banaction_allports = ufw

Così i blocchi finiscono come regole dentro UFW stesso e sopravvivono al suo ricaricamento. Controllo:

ufw status numbered

Gli indirizzi bloccati compaiono lì come DENY IN molto in alto nell'elenco. È importante che le regole di blocco stiano prima della tua apertura per la porta 22, altrimenti ha effetto per prima l'apertura. L'azione fornita di serie le inserisce perciò in testa con insert.

Chi non usa UFW e cura invece un proprio insieme di regole in /etc/nftables.conf ha un problema imparentato. Un flush ruleset all'inizio di quel file, così come lo propone il modello standard, al ricaricamento cancella anche f2b-table. O rinunci al flush globale e svuoti solo la tua tabella, oppure agganci il riavvio di fail2ban al servizio nftables. Il vantaggio della tabella propria di fail2ban è che con priorità filter - 1 agisce prima della normale tabella filter e quindi anche prima delle regole provenienti da iptables-nft.

Quello che non dovresti mischiare è banaction = nftables con un tuo insieme di regole iptables, aspettandoti che iptables -L mostri i blocchi. Le due strade funzionano in parallelo, ma ogni strumento mostra solo il proprio insieme di regole. Verifica sempre con lo strumento che corrisponde alla banaction impostata.

I messaggi di errore alla lettera e cosa ci sta dietro

I log li trovi, a seconda del sistema, in due posti:

journalctl -u fail2ban --no-pager -n 100
tail -n 100 /var/log/fail2ban.log

Failed during configuration: Have not found any log file for sshd jail

Il messaggio distintivo di Debian 12. Il jail gira con backend = auto, cerca /var/log/auth.log, e il file non esiste perché non è installato alcun rsyslog e tutto finisce nel journal. Il servizio non parte proprio, systemctl status fail2ban mostra Failed with result 'exit-code'. Due soluzioni: o apt install -y rsyslog e un riavvio, oppure in jail.local sotto [sshd] la riga backend = systemd. La seconda strada è la più moderna, ma richiede il pacchetto successivo.

Backend 'systemd' failed to initialize due to No module named 'systemd'

Hai impostato backend = systemd, ma manca il collegamento Python al journal. Su Debian 12 python3-systemd è solo raccomandato e nelle installazioni senza pacchetti raccomandati non viene tirato dentro. Rimedio:

apt install -y python3-systemd
systemctl restart fail2ban

Failed to access socket path: /var/run/fail2ban/fail2ban.sock. Is fail2ban running?

Il client non raggiunge il server. Nella stragrande maggioranza dei casi il servizio semplicemente non è in esecuzione, perché si è fermato su un errore di configurazione. Prima leggi systemctl status fail2ban, poi il journal. Di rado, dopo un'interruzione brusca, resta in giro un file socket orfano, e allora l'avvio del server segnala Server already running. In quel caso rimuovi il file socket e riavvia.

NOK: ('sshd',)

La risposta a fail2ban-client status sshd quando quel jail a runtime non esiste. O manca enabled = true, oppure hai sbagliato a scrivere il nome, oppure un errore di sintassi più in alto in jail.local si è mangiato la sezione. Quali jail girano davvero lo mostra fail2ban-client status. Quello che fail2ban ha assemblato da tutti i file lo mostra il dump della configurazione:

fail2ban-client -d

Error banning e iptables not found

Il jail conta correttamente, ma il blocco fallisce sull'azione. Tipico sulle installazioni Debian 12 snelle, dove è presente solo nftables mentre l'impostazione predefinita è iptables-multiport. O recuperi con apt install -y iptables, oppure in jail.local passi a banaction = nftables. La seconda strada è quella che Debian 13 e Ubuntu 24.04 percorrono comunque già da soli.

Quando non serve più niente: lo smontaggio ordinato

Poiché tutte le personalizzazioni stanno in jail.local, la via del ritorno è corta. Metti da parte il file, riavvia il servizio, e sei di nuovo alle impostazioni del pacchetto:

mv /etc/fail2ban/jail.local /root/jail.local.bak
systemctl restart fail2ban
fail2ban-client status

Se così funziona, l'errore è nella tua configurazione, e reinserisci i blocchi uno alla volta. Se non funziona nemmeno così, dipende dall'ambiente, quindi dalla sorgente di log mancante o dallo strumento firewall mancante. Proprio per questo jail.conf va lasciato intatto: questo smontaggio in dieci secondi è possibile solo finché il file del pacchetto è allo stato di fabbrica.

Per chiudere, l'inquadramento. fail2ban riduce il rumore nei log e ferma i tentativi di accesso lenti e ripetuti. Un attacco serio da una grande botnet usa ogni indirizzo una sola volta e passa accanto a qualsiasi limitazione di frequenza. Il passo più efficace contro il brute force su SSH resta la disattivazione dell'accesso con password a favore delle chiavi. E contro gli attacchi volumetrici aiuta comunque solo il filtraggio in rete, da noi con 3,2 Tbps di filtraggio Arbor in tempo reale direttamente nel datacenter di Francoforte sul Meno e fino a 17 Tbps di capacità di filtraggio globale nelle tariffe Professional. fail2ban è lo strato sottostante, sull'host, e lì svolge il suo compito in modo affidabile non appena tre cose sono a posto: la sorgente di log giusta, l'azione firewall giusta e una jail.local che, nel dubbio, puoi rimuovere di nuovo in un colpo solo.

Domande frequenti

Devo davvero lasciare jail.conf intatto?
Sì. Il file appartiene al pacchetto e può essere sostituito durante gli aggiornamenti. Tutte le personalizzazioni vanno in /etc/fail2ban/jail.local. Questo file viene letto dopo jail.conf e dopo jail.d/*.conf, e sovrascrive quindi anche le impostazioni di defaults-debian.conf.
Perché su Debian 12 fail2ban non parte dopo l'installazione?
Perché il jail sshd lavora con backend = auto e si aspetta /var/log/auth.log, che senza rsyslog installato non esiste. Nel journal compare allora "Failed during configuration: Have not found any log file for sshd jail". Soluzione: installare rsyslog oppure impostare backend = systemd e aggiungere il pacchetto python3-systemd.
Che cosa significa esattamente backend = auto?
auto prova nell'ordine pyinotify e polling. Entrambi sono metodi basati su file. auto non sceglie mai il journal di systemd. Chi vuole analizzare il journal deve impostare backend = systemd in modo esplicito.
Come tolgo di nuovo un blocco?
Singolarmente con fail2ban-client set sshd unbanip indirizzo-IP, su tutti i jail con fail2ban-client unban indirizzo-IP, e in caso di emergenza tutto in una volta con fail2ban-client unban --all. Chi deve restare libero in modo permanente va in ignoreip, seguito da fail2ban-client reload.
Perché fail2ban blocca, ma l'attaccante passa lo stesso?
Di solito nel frattempo UFW ha riscritto la tabella filter tramite iptables-restore, rimuovendo le chain di fail2ban. Dopo ogni ufw enable, disable o reload fail2ban va riavviato. Più pulito è banaction = ufw, così i blocchi vivono dentro UFW stesso.
Da che cosa capisco che fail2ban funziona davvero?
Non dal fatto che il servizio è in esecuzione. Verifica con fail2ban-regex se il filtro trova delle righe, e blocca a mano un indirizzo di prova preso da 203.0.113.0/24. Poi deve comparire nel firewall, in modo coerente con la banaction impostata: su Debian 13 e Ubuntu 24.04 con nft list table inet f2b-table nel set addr-set-sshd, su Debian 12 e Ubuntu 22.04 con iptables -n -L f2b-sshd nella catena omonima. Una catena f2b-sshd sui sistemi nftables non esiste.
fail2ban sostituisce una protezione DDoS?
No. fail2ban agisce sull'host contro i tentativi di accesso ripetuti di singoli indirizzi. Gli attacchi volumetrici vanno filtrati in rete, prima che raggiungano il server.

fail2ban SSH Sicurezza server Brute Force Debian Ubuntu nftables UFW