Configurare firewall avanzati con nftables su VPS gestite senza personale tecnico qualificato: guida operativa Debian e Ubuntu

Configurare firewall avanzati con nftables su VPS gestite senza personale tecnico qualificato: guida operativa Debian e Ubuntu

A maggio 2025, durante un audit di routine su un VPS Hetzner CX21 di un cliente emiliano - piccola azienda di consulenza gestionale con un portale Laravel per la gestione documentale dei clienti - ho scoperto che il server non aveva nessun firewall attivo. Nessuno. La porta 22 (SSH) era aperta a tutto internet, la porta 3306 (MySQL) era raggiungibile dall'esterno con bind-address = 0.0.0.0, la porta 6379 (Redis) rispondeva senza autenticazione, e non c'era né ufw, né iptables, né nftables configurato. Il server era online da quattordici mesi in quella condizione. Ho controllato i log SSH: 23.000 tentativi di autenticazione falliti nelle ultime 48 ore da IP distribuiti su tre continenti, con picchi di 800 tentativi all'ora. L'unica ragione per cui il server non era stato compromesso era che l'autenticazione SSH era a chiave - una configurazione che il freelance originario aveva fatto correttamente prima di sparire.

Quel VPS era una bomba a orologeria. Bastava un singolo errore - un utente con password debole creato per sbaglio, un servizio con credenziali di default - e l'intero patrimonio documentale dei clienti sarebbe stato esposto. In mezza giornata ho configurato nftables da zero, integrato Fail2ban, chiuso tutte le porte non necessarie e implementato il logging strutturato. In questo articolo ti racconto esattamente come, con la configurazione completa che uso come standard su ogni VPS Debian 12 con stack LEMP.

Stai cercando un Consulente Informatico esperto per mettere in sicurezza il tuo VPS? Nel mio profilo professionale trovi l'esperienza concreta su hardening Linux, nftables e sicurezza infrastrutturale per PMI. Contattami per una consulenza diretta.

Perché nftables e non ufw o iptables?

Su Debian 12 Bookworm, nftables è il framework firewall predefinito del kernel, e il progetto Debian raccomanda esplicitamente di costruire nuove configurazioni firewall su nftables anziché su iptables. ufw (Uncomplicated Firewall) è un frontend per iptables che semplifica la gestione di regole base, e va benissimo per configurazioni semplici - ma quando hai bisogno di rate limiting per IP, integrazione con Fail2ban, set dinamici di IP bannati e logging granulare, ufw diventa un collo di bottiglia e nftables è la scelta corretta.

I vantaggi concreti di nftables per un VPS in produzione sono tre. Primo: un unico strumento (nft) gestisce IPv4, IPv6, ARP e bridging con la stessa sintassi - niente più doppi set di regole iptables/ip6tables. Secondo: le regole vengono compilate in bytecode che il kernel esegue direttamente, riducendo l'overhead per pacchetto rispetto all'interpretazione sequenziale di iptables. Terzo: i set e le mappe di nftables permettono gestione efficiente di grandi liste di IP (come quelle generate da Fail2ban) con aggiornamento atomico, senza ricostruire l'intera chain.

La configurazione base: default-deny per stack LEMP

Il principio fondamentale di un firewall è default-deny: tutto ciò che non è esplicitamente permesso è bloccato. La configurazione che installo su ogni VPS Debian 12 con Nginx, PHP-FPM, MySQL e Redis è questa:

#!/usr/sbin/nft -f
# /etc/nftables.conf - configurazione standard VPS LEMP
# Autore: Maurizio Fonte - ultimo aggiornamento 2025-07

flush ruleset

table inet filter {

    # Set per IP fidati (gestione SSH)
    set trusted_ssh {
        type ipv4_addr
        flags interval
        elements = {
            93.XX.XX.XX,        # IP ufficio consulente
            85.YY.YY.YY         # IP ufficio cliente
        }
    }

    chain input {
        type filter hook input priority 0; policy drop;

        # Connessioni già stabilite: accettare sempre
        ct state established,related accept

        # Pacchetti invalidi: drop immediato (evasion prevention)
        ct state invalid drop

        # Loopback: accettare (PHP-FPM, MySQL socket, Redis locale)
        iif lo accept

        # ICMP: accettare con rate limiting (diagnostica, ma no flood)
        ip protocol icmp icmp type { echo-request, echo-reply } \
            limit rate 5/second accept

        # SSH: solo da IP fidati
        tcp dport 22 ip saddr @trusted_ssh accept

        # HTTP e HTTPS: aperti a tutti
        tcp dport { 80, 443 } accept

        # Logging dei pacchetti droppati (per diagnostica)
        log prefix "NFT_DROP: " level info limit rate 5/minute
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

Ogni riga ha una ragione specifica. Il ct state invalid drop è la prima regola dopo established,related perché i pacchetti con stato invalido sono spesso tentativi di evasione (pacchetti fuori sequenza, SYN-ACK senza SYN precedente) e vanno scartati prima di qualsiasi altra valutazione. Il set trusted_ssh limita l'accesso SSH solo agli IP autorizzati - questo è il singolo cambiamento che elimina il 99% dei tentativi di brute force. Il rate limiting su ICMP permette il ping diagnostico ma impedisce flood. Le porte 80 e 443 sono aperte a tutti perché il traffico web deve essere accessibile. MySQL (3306), Redis (6379) e PHP-FPM (9000) non appaiono: sono raggiungibili solo via socket locale o su 127.0.0.1, mai dall'esterno.

Il logging con limit rate 5/minute evita che un port scan massiccio saturi il syslog - senza il limit, un attaccante che scansiona tutte le 65.535 porte genererebbe 65.535 righe di log in pochi secondi.

Come applicare la configurazione senza bloccarsi fuori

Il rischio più grande quando configuri un firewall via SSH è bloccarti fuori dal server - se sbagli una regola e SSH viene bloccato, hai perso l'accesso. La procedura sicura che uso sempre è:

# 1. Salvare la configurazione in /etc/nftables.conf
nano /etc/nftables.conf  # incollare la configurazione sopra

# 2. Verificare la sintassi PRIMA di applicare
nft -c -f /etc/nftables.conf
# Se restituisce errori, correggere PRIMA di procedere

# 3. Applicare con timeout di sicurezza
# Se perdi la connessione, dopo 60 secondi le regole vengono resettate
nft -f /etc/nftables.conf && sleep 60 && nft flush ruleset

# 4. Se tutto funziona (SSH risponde), interrompere con Ctrl+C
# e rendere le regole persistenti
systemctl enable nftables
systemctl restart nftables

# 5. Verificare che le regole siano attive
nft list ruleset

Il trucco è nella riga 3: nft -f /etc/nftables.conf && sleep 60 && nft flush ruleset. Se la connessione SSH si interrompe dopo l'applicazione delle regole (perché ti sei bloccato fuori), il sleep 60 non viene mai raggiunto dal tuo terminale, e dopo 60 secondi il comando nft flush ruleset viene eseguito, resettando tutte le regole e ripristinando l'accesso. Se invece tutto funziona, premi Ctrl+C durante il sleep per interrompere il flush e mantenere le regole attive. Questo metodo mi ha salvato da almeno tre lockout accidentali negli anni.

Integrazione con Fail2ban per ban dinamici

nftables gestisce le regole statiche (chi può accedere a cosa), Fail2ban gestisce i ban dinamici (chi ha fatto troppi tentativi sbagliati viene bloccato automaticamente). L'integrazione tra i due su Debian 12 richiede una configurazione specifica di Fail2ban per usare nftables come backend:

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

[sshd]
enabled = true
port = ssh
maxretry = 3
findtime = 600
bantime = 3600

[nginx-http-auth]
enabled = true
port = http,https
maxretry = 5
findtime = 300
bantime = 1800

Quando Fail2ban banna un IP, crea automaticamente un set nftables con gli IP bannati e una regola che li droppa. Puoi vedere i ban attivi con:

# IP attualmente bannati da Fail2ban via nftables
nft list set inet f2b-table addr-set-sshd 2>/dev/null
fail2ban-client status sshd

Ho descritto in dettaglio la configurazione di Fail2ban per applicazioni Laravel - incluse le jail custom per il login web - nell'articolo su Fail2ban fermo su VPS con Laravel.

IPv6: il firewall che tutti dimenticano

Un errore che trovo in almeno metà dei VPS che audito: il firewall è configurato solo per IPv4. Su Hetzner, OVH e Digital Ocean, ogni VPS riceve sia un IPv4 che un IPv6 (o un intero blocco /64). Se le regole nftables coprono solo IPv4, l'intera superficie di attacco IPv6 resta completamente esposta - SSH, MySQL, Redis, tutto raggiungibile da chiunque via IPv6.

La soluzione è usare la famiglia inet (non ip) nelle tabelle nftables. La configurazione che ho mostrato sopra usa già table inet filter - la keyword inet applica le regole sia a IPv4 che a IPv6 contemporaneamente. Se la tua configurazione usa table ip filter, coprirà solo IPv4 e dovrai aggiungere una table ip6 filter separata o, meglio, migrarla a inet.

# Verificare se il VPS ha un indirizzo IPv6
ip -6 addr show | grep "scope global"

# Se ha IPv6, verificare che le regole lo coprano
nft list ruleset | head -3
# Deve mostrare "table inet filter", non "table ip filter"

Se il tuo VPS ha IPv6 ma non lo usi per le applicazioni, la scelta più sicura è disabilitarlo completamente a livello di kernel:

# Disabilitare IPv6 se non necessario
echo "net.ipv6.conf.all.disable_ipv6 = 1" >> /etc/sysctl.conf
echo "net.ipv6.conf.default.disable_ipv6 = 1" >> /etc/sysctl.conf
sysctl -p

Gli errori più comuni nella configurazione nftables su VPS

Nei miei audit trovo sempre gli stessi errori. Il primo è lasciare il policy accept sulla chain input - il che rende il firewall decorativo: accetta tutto e le regole di drop servono solo come ornamento. La policy corretta per la chain input è sempre drop.

Il secondo errore è non gestire i pacchetti ct state invalid. Senza quella regola, pacchetti malformati o fuori sequenza - che sono spesso sonde di un attaccante - attraversano tutte le regole della chain e vengono processati normalmente. Il ct state invalid drop appena dopo established,related accept li ferma subito.

Il terzo errore è esporre servizi che devono essere solo locali. MySQL deve sempre avere bind-address = 127.0.0.1 nel suo my.cnf, Redis deve avere bind 127.0.0.1 nel suo redis.conf, e PHP-FPM deve ascoltare su un socket Unix (listen = /run/php/php8.2-fpm.sock) anziché su una porta TCP. Il firewall è la seconda linea di difesa - la prima è che il servizio non ascolti proprio sull'interfaccia pubblica.

Il quarto errore è dimenticare di rendere le regole persistenti. Su Debian 12, nft -f /etc/nftables.conf applica le regole alla sessione corrente, ma se il server viene riavviato senza systemctl enable nftables, le regole spariscono. Ho visto server con firewall "configurato" che dopo un riavvio (per manutenzione, aggiornamento kernel, o crash) tornavano completamente aperti.

Gestione operativa: aggiungere e rimuovere regole in produzione

In produzione capita di dover aprire temporaneamente una porta - per un debug, per un servizio terzo, per un test di un'integrazione. La procedura corretta è aggiungere la regola a runtime con nft add, testare, e poi decidere se renderla permanente aggiornando /etc/nftables.conf:

# Aggiungere una regola temporanea (es. porta 8080 per test)
nft add rule inet filter input tcp dport 8080 accept

# Verificare che sia attiva
nft list chain inet filter input

# Se il test è completato, rimuovere la regola
# (serve l'handle number, trovabile con -a)
nft -a list chain inet filter input | grep 8080
# Output: tcp dport 8080 accept # handle 42
nft delete rule inet filter input handle 42

# Se la regola deve restare, aggiornare /etc/nftables.conf
# e ricaricare: nft -f /etc/nftables.conf

Non modificare mai /etc/nftables.conf e ricaricare per aggiungere una regola temporanea - se il reload fallisce per un errore di sintassi, perdi tutte le regole e il server resta esposto.

Cosa è cambiato sul VPS emiliano dopo la configurazione

I numeri parlano da soli. Prima di nftables: 23.000 tentativi SSH in 48 ore, MySQL raggiungibile dall'esterno, Redis senza autenticazione esposto a internet. Dopo: zero tentativi SSH da IP non autorizzati (perché SSH è accessibile solo da due IP fidati), MySQL e Redis raggiungibili solo su localhost, Fail2ban che banna automaticamente i pochi IP che tentano di forzare i servizi web. Il tutto con un impatto sulle performance del server inferiore all'1% - nftables è estremamente efficiente a livello di kernel.

La configurazione che ho descritto è il punto di partenza. Per un hardening completo - che include anche la configurazione di SSH, le policy di aggiornamento, il monitoring e l'audit periodico - rimando alla checklist di hardening in quattordici giorni e all'articolo sull'hardening di server Debian e Ubuntu per applicativi PHP. Il firewall è il primo strato della difesa, non l'unico.

Se il tuo VPS è in produzione senza un firewall configurato - o con un firewall configurato anni fa e mai rivisto - sei esposto a rischi che crescono ogni giorno. Nel mio lavoro di consulente ho trovato server con MySQL esposto a internet in almeno un caso su tre degli audit iniziali su PMI italiane. La buona notizia è che configurare nftables su Debian 12 richiede meno di un'ora di lavoro e il risultato è una riduzione drastica della superficie di attacco. Se vuoi un audit della sicurezza della tua infrastruttura - o se hai un VPS in produzione che non ha mai avuto un firewall configurato correttamente - contattami. Un'ora di configurazione nftables oggi può evitare settimane di incident response domani.

Ultima modifica: