Fail2ban fermo da mesi su un VPS con Laravel: come un brute force SSH da 14.000 tentativi al giorno è passato inosservato

Fail2ban fermo da mesi su un VPS con Laravel: come un brute force SSH da 14.000 tentativi al giorno è passato inosservato

Ad agosto 2025 ho eseguito un audit di sicurezza su un VPS Digital Ocean Premium (8 vCPU, 16 GB RAM, 320 GB NVMe) che ospitava un SaaS Laravel 10 per la gestione di prenotazioni nel settore sanitario - circa 120 studi medici come clienti e 15.000 utenti finali. Il SaaS gestiva dati sanitari: anagrafiche pazienti, appuntamenti, referti. Il titolare mi aveva contattato per una consulenza NIS2, non per un problema di sicurezza. Ma il primo controllo che faccio in ogni audit è verificare lo stato delle protezioni attive:

systemctl is-active fail2ban
# → inactive (dead)

systemctl is-enabled fail2ban
# → disabled

Fail2ban era installato ma non girava. Non girava da sei mesi - da un aggiornamento di sistema che aveva resettato lo stato dei servizi senza riabilitarli. Per sei mesi, il VPS era stato esposto senza nessuna protezione contro brute force.

Ho controllato l'auth log di SSH:

grep "Failed password" /var/log/auth.log | wc -l
# → 87.432 (nei 6 giorni di log disponibili - log rotation settimanale)

# Media giornaliera
echo "87432 / 6" | bc
# → 14.572 tentativi al giorno

Quattordicimila tentativi di brute force SSH al giorno. Ogni giorno. Per sei mesi. I tentativi fallivano perché l'autenticazione SSH era configurata solo con chiave pubblica (niente password) - quindi nessun rischio diretto di compromissione. Ma quei 14.000 tentativi giornalieri consumavano risorse del server (ogni tentativo è un processo sshd che deve validare e rifiutare la connessione), riempivano i log, e soprattutto mascheravano eventuali tentativi più sofisticati. Se un attaccante avesse provato un exploit SSH (come il regreSSHion CVE-2024-6387 di cui ho parlato nell'articolo sull'aggiornamento di sicurezza di server Debian), i suoi tentativi si sarebbero persi nel rumore di 14.000 brute force.

C'è un aspetto dei brute force SSH che i titolari non vedono: ogni tentativo, anche se fallisce, consuma risorse. Il processo sshd deve fare il fork, negoziare la connessione, validare (e rifiutare) la chiave o la password, e chiudere la connessione. Con 14.000 tentativi al giorno - circa 10 al minuto - l'impatto sulle performance è misurabile ma non catastrofico. Ma durante un attacco coordinato, i tentativi possono salire a migliaia al minuto, e a quel punto il consumo di CPU e la saturazione delle connessioni SSH possono degradare le performance del server anche se nessun tentativo ha successo. Fail2ban risolve il problema alla radice: dopo 3 tentativi falliti, l'IP viene bannato a livello firewall e le connessioni successive vengono rifiutate dal kernel prima ancora di raggiungere sshd - costo computazionale vicino a zero.

Ma il problema più grave non era SSH - era il login web. L'applicazione Laravel aveva un form di login su /login senza nessuna protezione: niente rate limiting, niente captcha, niente Fail2ban. Un attaccante poteva provare migliaia di combinazioni username/password senza nessuna conseguenza. Su un SaaS che gestisce dati sanitari, questa è una violazione grave di ogni principio di sicurezza - e un finding che in un audit NIS2 viene classificato come critico.

Perché Fail2ban smette di funzionare e nessuno se ne accorge?

Fail2ban è un servizio che gira in background. Quando funziona, non produce output visibile - nessun'email, nessuna notifica, nessuna dashboard. L'unico modo per sapere che funziona è controllare attivamente il suo stato. Quando smette di funzionare - per un aggiornamento di sistema, un conflitto di dipendenze, un errore nella configurazione dei jail - la sua assenza è altrettanto invisibile. Nessun alert, nessun messaggio, nessun sintomo. Il server continua a funzionare normalmente, gli utenti non notano nulla. L'unica differenza è che ogni IP che prova un brute force non viene più bannato.

Le cause di fallimento più comuni che trovo sui VPS di PMI:

  • Aggiornamento di sistema che resetta lo stato dei servizi (apt dist-upgrade su Debian/Ubuntu può disabilitare servizi che erano stati abilitati manualmente).
  • Conflitto con il backend di ban - Fail2ban supporta iptables, nftables e firewalld. Se il sistema passa da iptables a nftables (come è successo di default su Debian 11), i ban di Fail2ban smettono di funzionare silenziosamente.
  • Log path errato - se l'applicazione cambia la posizione dei log (da /var/log/auth.log a /var/log/syslog, o un cambio di versione di SSH che modifica il formato), il jail di Fail2ban non trova più i pattern e non banna nessuno.
  • Regex non aggiornata - le nuove versioni di OpenSSH possono cambiare il formato dei messaggi di log. Se la regex del filtro sshd non corrisponde, Fail2ban legge il log ma non riconosce i tentativi falliti.

Stai cercando un Consulente Informatico esperto per hardenizzare la protezione brute force sulla tua infrastruttura Laravel? Nel mio profilo professionale trovi l'esperienza concreta su Fail2ban, sicurezza SSH e protezione di applicazioni web su VPS per PMI.

Riattivazione e hardening: SSH, login web e ban progressivo

La riattivazione base è banale - systemctl enable --now fail2ban. Ma riattivare Fail2ban con la configurazione di default è come mettere un lucchetto di cartone: tecnicamente presente, praticamente inutile. La configurazione che applico su ogni VPS copre tre livelli.

Jail SSH hardenizzata

# /etc/fail2ban/jail.local
[sshd]
enabled  = true
port     = ssh
filter   = sshd
logpath  = %(sshd_log)s
maxretry = 3
bantime  = 1h
findtime = 10m

Con autenticazione solo a chiave (che deve essere il default su qualunque VPS di produzione), maxretry = 3 è aggressivo - un utente legittimo non fallisce mai perché non usa password. Ogni tentativo è un bot, e va bannato dopo 3 tentativi. Il bantime di 1 ora è sufficiente per i bot semplici che cambiano IP dopo il ban.

Jail custom per il login web Laravel

Questo è il pezzo che manca nella maggior parte delle installazioni Fail2ban e che è il più critico per le applicazioni web. La guida di Joe Campo sulla protezione delle rotte Laravel con Fail2ban mostra il pattern: creare un filtro custom che matcha i tentativi POST su /login nel log di Nginx, e un jail che banna dopo un numero ragionevole di tentativi:

# /etc/fail2ban/filter.d/laravel-login.conf
[Definition]
failregex = ^<HOST> .* "POST /login HTTP.*" (401|422)
ignoreregex =
# /etc/fail2ban/jail.local (aggiungere)
[laravel-login]
enabled  = true
filter   = laravel-login
action   = iptables-multiport[name=LaravelLogin, port="http,https"]
logpath  = /var/log/nginx/access.log
bantime  = 30m
findtime = 2m
maxretry = 10

La regex matcha i POST a /login che ricevono 401 (Unauthorized) o 422 (Validation Error - il codice che Laravel restituisce per credenziali errate). 10 tentativi falliti in 2 minuti = ban per 30 minuti. Sufficientemente permissivo per un utente che sbaglia password tre volte, sufficientemente restrittivo per bloccare un brute force automatizzato.

Prima di attivare il jail, verifica che la regex funzioni con il formato del tuo log:

fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/laravel-login.conf

Ban progressivo con recidive

Per i bot persistenti che tornano dopo il ban, il jail recidive aumenta progressivamente la durata del ban:

# /etc/fail2ban/jail.local (aggiungere)
[recidive]
enabled  = true
logpath  = /var/log/fail2ban.log
banaction = iptables-allports
bantime  = 1w
findtime = 1d
maxretry = 3

Se un IP viene bannato 3 volte in 24 ore (su qualunque jail), il ban diventa di 1 settimana su tutte le porte. La combinazione di Fail2ban con autenticazione a chiave SSH e ban progressivo rende il brute force praticamente impossibile sia sull'SSH che sulle rotte web dell'applicazione.

Per chi gestisce server con applicazioni Laravel e vuole una protezione stratificata - non solo Fail2ban ma anche rate limiting a livello Nginx, Cloudflare e applicativo - ho descritto il caso reale di un attacco di credential stuffing da 14.000 richieste al minuto contenuto in 47 minuti con Nginx, Fail2ban e Cloudflare.

Whitelist, monitoring e gli errori da evitare

Tre configurazioni aggiuntive che fanno la differenza tra un Fail2ban che funziona e uno che crea problemi:

Whitelist degli IP legittimi. Senza whitelist, Fail2ban può bannare l'IP del tuo ufficio, il tuo CI/CD server, o il tuo monitoring. Sul SaaS del cliente sanitario, l'IP dell'ufficio era statico - l'ho aggiunto immediatamente:

# /etc/fail2ban/jail.local - sezione [DEFAULT]
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 IP.UFFICIO.QUI IP.CI.CD.QUI

Se l'IP dell'ufficio è dinamico (ADSL/fibra senza IP statico), la whitelist va gestita con un range più ampio o con una soluzione alternativa come un port knocking per SSH.

Backend corretto. Su Debian 11+ e Ubuntu 22.04+ il firewall di default è nftables, non iptables. Ma Fail2ban per default usa iptables come azione di ban. Il risultato: Fail2ban pensa di aver bannato un IP, ma il ban non ha effetto perché iptables e nftables non condividono le stesse tabelle. La verifica:

# Quale backend usa Fail2ban?
fail2ban-client get sshd actions

# I ban sono effettivamente attivi nel firewall?
iptables -L f2b-sshd -n 2>/dev/null || nft list ruleset | grep f2b

Se usi nftables, aggiorna la configurazione di Fail2ban:

# /etc/fail2ban/jail.local
[DEFAULT]
banaction = nftables-multiport
banaction_allports = nftables-allports

Monitoring dello stato di Fail2ban. Fail2ban che smette di funzionare è invisibile - l'ho detto all'inizio di questo articolo e lo ripeto perché è il punto più importante. L'unico modo per sapere che Fail2ban è attivo è un check periodico. Il più semplice: un cron job che verifica lo stato e manda un alert se il servizio è morto:

# /etc/cron.d/check-fail2ban
*/5 * * * * root systemctl is-active fail2ban > /dev/null 2>&1 || \
    curl -s "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
    -d chat_id="${CHAT_ID}" -d text="ALERT: Fail2ban è morto su $(hostname)"

Con Prometheus, il check è più elegante: un exporter Fail2ban che espone le metriche (IP bannati, tentativi bloccati, stato dei jail) e un alert che scatta quando il servizio non risponde. Ho descritto lo stack completo di alerting nel mio articolo sul monitoring proattivo per Laravel su VPS.

Un ultimo errore che vedo ripetere: bannare per troppo tempo al primo tentativo. Un bantime = 24h al primo ban sembra aggressivo ma sicuro - fino a quando l'IP bannato è un resolver NAT aziendale dietro cui ci sono 200 dipendenti del tuo cliente. Un utente sbaglia password, l'IP viene bannato per 24 ore, e 200 persone non possono più accedere al portale. La strategia corretta è ban brevi al primo livello (30 minuti - 1 ora), ban progressivamente più lunghi con il jail recidive (1 giorno → 1 settimana), e whitelist degli IP dei clienti noti.

Sul VPS del cliente sanitario, dopo la riattivazione e la configurazione dei tre jail (SSH, login web, recidive), i 14.000 tentativi SSH giornalieri sono scesi a zero nel giro di 72 ore - i bot che venivano bannati dopo 3 tentativi smettevano di tornare dopo il terzo ban progressivo. I tentativi di login web, che prima non venivano nemmeno contati, hanno rivelato circa 200 tentativi al giorno su 8 account utente diversi (tutti con email pubbliche presenti nel sito degli studi medici) - tutti bloccati dopo 10 tentativi. Il finding è stato incluso nel report NIS2 come "misura correttiva implementata" - un dato positivo nell'audit perché dimostra capacità di rilevare e correggere vulnerabilità. Se il brute force fosse andato avanti senza protezione fino a un'eventuale compromissione, lo stesso finding sarebbe stato un "incidente non rilevato" - la peggior classificazione possibile in un audit di conformità.

Il costo dell'intero intervento Fail2ban - riattivazione, configurazione tre jail, whitelist, verifica backend, monitoring - è stato di circa 3 ore di lavoro. Il costo di un data breach su dati sanitari causato da un brute force su un login senza protezione - secondo le stime del Garante Privacy per violazioni GDPR su dati di categoria particolare (art. 9) - può arrivare a centinaia di migliaia di euro tra sanzione, notifica agli interessati, danni reputazionali e remediation post-incidente. Tre ore di prevenzione contro sei mesi di conseguenze.

Se Fail2ban è installato sul tuo VPS ma non l'hai mai verificato con systemctl is-active fail2ban, fallo adesso. Se è attivo, controlla che i jail siano effettivamente operativi con fail2ban-client status. Se il tuo applicativo Laravel ha un form di login senza protezione brute force - e nella maggior parte dei progetti che vedo è così - una jail custom Fail2ban è il fix più rapido e più efficace che puoi implementare in un'ora. Il rate limiter nativo di Laravel (ThrottleRequests middleware) è un complemento utile a livello applicativo, ma non sostituisce Fail2ban: il throttle di Laravel rallenta l'attaccante con 429 Too Many Requests, Fail2ban lo blocca a livello firewall prima che la richiesta arrivi anche solo a Nginx. Sono due livelli diversi di difesa e vanno usati insieme. Contattami se hai bisogno di un hardening Fail2ban completo: in mezza giornata configuro jail SSH, jail custom per le rotte critiche di Laravel, ban progressivo con recidive, e monitoring continuo dello stato dei ban integrato con il sistema di alerting esistente.

Ultima modifica: