Prima di leggere: una domanda che si risponde con un numero
Nella lezione precedente hai imparato che curl lascia codice di uscita 6
quando non risolve un nome e 7 quando non riesce a connettersi. Adesso la domanda: un
firewall che scarta i pacchetti e un firewall che li rifiuta producono lo stesso codice di uscita
nel client? E se no, quale produce quale?
Scrivi la risposta. Questa differenza non è un dettaglio da manuale: è il modo in cui, davanti a un servizio che non risponde, distingui «c'è un firewall in mezzo» da «non c'è nessuno in ascolto» senza avere accesso al server. E si misura in secondi.
Il problema: il firewall è l'unico strumento che ti chiude fuori
Quasi tutti gli errori di amministrazione si correggono rifacendo il comando. Un errore di firewall su una macchina remota, no: il comando che sbagli è anche il comando che ti impedisce di collegarti per correggerlo. È l'unico caso in cui la sequenza delle operazioni conta più del loro contenuto.
A questo si aggiunge una confusione che rende tutto più difficile: su Linux esistono tre nomi per la stessa cosa, e sembrano tre strumenti alternativi. Non lo sono. Sono tre livelli impilati, e sapere quale stai usando determina dove guardare quando una regola non fa quello che dovrebbe.
Il laboratorio della lezione precedente serve anche qui, e in modo ancora più utile: dentro un namespace di rete puoi scrivere la regola che ti chiuderebbe fuori da un server, vederla funzionare, e uscire chiudendo la shell.
Tre nomi, un solo motore
Il filtraggio dei pacchetti sta nel kernel, nel sottosistema netfilter. Il programma che gli
parla direttamente è nft, del progetto nftables. Sopra c'è iptables, che
è la vecchia interfaccia, oggi reimplementata per tradurre nel linguaggio di nftables. Sopra ancora
c'è ufw, che non è un firewall ma un generatore di regole con una sintassi breve.
Che iptables sia un traduttore non è una tesi: lo dichiara da sé. Su questa macchina,
misurato il 2026-10-30:
$ iptables --version
iptables v1.8.11 (nf_tables)
$ iptables-legacy --version
iptables v1.8.11 (legacy)
$ nft --version
nftables v1.1.6 (Commodore Bullmoose #7)
La parentesi dopo il numero di versione dice quale motore sta dietro. Il primo comando è la versione
che scrive in nftables; esiste ancora, con un altro nome, quella che scrive nel vecchio motore. E la
traduzione si vede: una regola scritta con iptables compare in
nft list ruleset, con un avviso che vale la pena leggere.
# iptables -A INPUT -p icmp --icmp-type echo-request -j DROP
# iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-A INPUT -p icmp -m icmp --icmp-type 8 -j DROP
# nft list ruleset
# Warning: table ip filter is managed by iptables-nft, do not touch!
table ip filter {
chain INPUT {
type filter hook input priority filter; policy accept;
ip protocol icmp icmp type echo-request counter packets 0 bytes 0 drop
}
}
L'avviso è la cosa più importante di questo blocco: do not touch. Le due interfacce
scrivono nello stesso motore ma non si coordinano, e modificare con nft una tabella
creata da iptables produce uno stato che il secondo non si aspetta. La regola pratica:
su una macchina si usa un solo livello, e il primo compito su una macchina che non
conosci è scoprire quale.
La regola di valutazione, che è una sola
Una catena di regole si valuta dall'alto, e la prima che corrisponde decide. Se nessuna corrisponde, decide la politica predefinita della catena. Questo è tutto il modello, e da questo derivano tutti gli errori.
La pagina di manuale di ufw presente sul sistema, nella versione datata maggio 2023, lo
dice in una frase che vale la pena citare per intero: «Rule ordering is important and the first match
wins. Therefore when adding rules, add the more specific rules first with more general rules later».
Quindi non è una particolarità di nft: è il modello di tutti e tre i livelli.
La politica predefinita è l'altra metà. Su questa macchina il file
/etc/default/ufw la dichiara così, e la direzione del traffico cambia la risposta:
$ grep -E '^DEFAULT_(INPUT|OUTPUT|FORWARD)_POLICY' /etc/default/ufw
DEFAULT_INPUT_POLICY="DROP"
DEFAULT_OUTPUT_POLICY="ACCEPT"
DEFAULT_FORWARD_POLICY="DROP"
In ingresso si scarta tutto ciò che non è esplicitamente permesso, in uscita si permette tutto. È il modello che vuoi su un server esposto, ed è anche il modello che ti chiude fuori se attivi il firewall prima di aver permesso la porta da cui sei collegato.
Il confronto che risponde alla domanda d'apertura
Due modi di non far passare un pacchetto: drop lo butta e non dice niente,
reject risponde al mittente che la porta non è raggiungibile. Il server si comporta in
modo diverso, e la differenza la vede il client. Misurata nel laboratorio isolato, con un servizio
in ascolto sulla porta 8080:
# nft add rule inet prova input tcp dport 8080 counter drop
# time curl -s -o /dev/null --max-time 3 http://127.0.0.1:8080/
tempo: 3.00 s
exit di curl=28
# nft flush chain inet prova input
# nft add rule inet prova input tcp dport 8080 counter reject
# time curl -sS -o /dev/null --max-time 3 http://127.0.0.1:8080/
curl: (7) Failed to connect to 127.0.0.1 port 8080 after 0 ms: Could not connect to server
tempo: 0.00 s
exit di curl=7
Con drop il client aspetta e scade: tre secondi, che sono il limite che gli avevo
imposto, e codice 28. Con reject il rifiuto arriva subito: zero secondi e
codice 7, lo stesso che avresti se nessuno fosse in ascolto. Quindi la risposta alla
domanda d'apertura è no, i due codici sono diversi, e il codice 28 con un'attesa che
arriva al limite è la firma di un firewall che scarta.
La scelta fra i due non è ovvia e dipende dallo scopo. drop non rivela nulla a chi
esplora la tua macchina, e per questo è il predefinito sui server esposti; il costo è che un client
legittimo mal configurato attende invano. reject dà una risposta utile a chi diagnostica
ed è più gentile sulle reti interne; il costo è che conferma a chiunque che la macchina esiste.
L'errore di classificazione plausibile è pensare che drop sia «più sicuro» in senso
assoluto: è più discreto, che non è la stessa cosa, e su una rete interna la discrezione la paghi in
ore di diagnosi.
Esempio svolto: l'ordine delle regole, provato e non raccontato
Il problema: hai due regole sulla stessa porta, una che permette e una che blocca. Vuoi sapere quale vince, e vuoi saperlo per misura e non per deduzione.
L'informazione disponibile è la regola di valutazione citata sopra. Il ragionamento: se la prima che corrisponde decide, allora invertendo l'ordine delle due regole il risultato deve cambiare, e la prova si fa con una richiesta vera a un servizio vero. Il laboratorio è un namespace isolato con un server minimo in ascolto sulla 8080.
# curl -s -o /dev/null -w "codice=%{http_code}\n" --max-time 2 http://127.0.0.1:8080/
codice=200
# nft add rule inet prova input tcp dport 8080 drop
# nft add rule inet prova input tcp dport 8080 accept
# curl -s -o /dev/null -w "codice=%{http_code}\n" --max-time 2 http://127.0.0.1:8080/
codice=000
# nft flush chain inet prova input
# nft add rule inet prova input tcp dport 8080 accept
# nft add rule inet prova input tcp dport 8080 drop
# curl -s -o /dev/null -w "codice=%{http_code}\n" --max-time 2 http://127.0.0.1:8080/
codice=200
Senza regole il servizio risponde 200. Con drop davanti ad
accept non risponde affatto. Con accept davanti a drop risponde
di nuovo. Le due catene contengono esattamente le stesse due regole: cambia solo l'ordine, e cambia
il risultato.
La decisione che prendo per il lavoro reale: non aggiungo mai una regola «in fondo» senza aver letto
la catena. L'alternativa, cioè fidarsi del fatto che la regola nuova sia più specifica e quindi
prevalga, è sbagliata in modo subdolo: la specificità non conta, conta la posizione. Una regola
perfetta in fondo a una catena che ha un drop generale sopra non viene mai valutata, e
non c'è nessun messaggio che te lo dica.
La verifica che chiude il caso è lo strumento più sottovalutato di nftables, il contatore. Aggiungendo
la parola counter a una regola, il kernel conta i pacchetti che la attraversano, e li
puoi leggere:
# nft list chain inet prova input
table inet prova {
chain input {
type filter hook input priority filter; policy accept;
tcp dport 8080 counter packets 1 bytes 60 reject
}
}
Un pacchetto, sessanta byte. Se una regola che credi attiva mostra packets 0 dopo che
hai provato a usarla, la regola non è stata raggiunta, e il problema sta sopra di lei. È la
differenza fra diagnosticare un firewall e indovinare: l'errore che avrei potuto commettere, e che
ho visto fare molte volte, è riscrivere la regola che non funziona invece di chiedersi se viene
valutata.
Simulazione professionale: stai per chiuderti fuori
Scenario. Lavori su un server a cui accedi soltanto via rete, con una sessione remota aperta. Il responsabile ti chiede di attivare il firewall con la politica che hai visto, tutto chiuso in ingresso. Hai davanti questi quattro comandi, in quest'ordine. Prima di leggere il seguito, dimmi che cosa succede e a quale comando.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable
sudo ufw allow 22/tcp
La sessione cade al terzo comando. I primi due impostano le politiche ma non attivano niente: sono
dichiarazioni. Il terzo attiva il firewall, e in quel momento la politica in ingresso è
deny e non esiste nessuna deroga per la porta da cui sei collegato. Il quarto comando è
corretto, è anche necessario, e non lo eseguirai mai perché non avrai più una sessione da cui
scriverlo.
L'errore è plausibile per tre ragioni che si sommano, e vale la pena nominarle tutte. La prima è che
l'ordine sembra logico: prima le politiche, poi l'accensione, poi i dettagli. La seconda è che
ufw enable emette un avviso sulla possibile interruzione delle connessioni esistenti, e
un avviso visto cento volte non si legge più. La terza è che su una macchina locale questa sequenza
non fa nessun danno, quindi chi l'ha provata in laboratorio l'ha vista funzionare.
La sequenza corretta inverte le ultime due righe: allow prima di enable.
E anche con l'ordine giusto, il lavoro non è finito: prima di attivare conviene verificare la regola
con ufw --dry-run enable, che mostra le regole che verrebbero scritte senza scriverle,
e prevedere una via di fuga che non dipende dalla rete. Su un server virtuale è la console del
fornitore; su una macchina fisica è l'accesso locale; se nessuna delle due è disponibile, un lavoro
periodico che disattiva il firewall dopo dieci minuti è una rete di sicurezza grossolana e che
funziona.
Una precisazione sulla porta, perché l'esempio usa il numero per chiarezza: la porta
22 è quella consueta per le sessioni remote, ma la configurazione del tuo server può
usarne un'altra, e il comando va scritto sulla porta vera. Il modo di saperla senza indovinare è
guardare quale porta ha la connessione da cui sei collegato in questo momento, con
ss -tnp, che è lo strumento della prossima lezione.
Esercizio 1, guidato: una regola che corrisponde e non basta
Nel laboratorio isolato, avvia un servizio in ascolto sulla porta 8080 e costruisci una catena con
politica drop in ingresso e una sola regola, tcp dport 8080 counter accept.
Prova con curl, poi leggi il contatore della regola. Scrivi che cosa ti aspetti prima di
eseguire, e poi spiega il risultato: il contatore ti dirà una cosa che il codice di uscita di
curl sembra contraddire.
Esercizio 2, autonomo: la regola che non viene mai valutata
Costruisci, nel laboratorio, una catena in cui una regola corretta non ha nessun effetto perché una regola sopra di lei la precede. Poi, senza modificare l'ordine, dimostra con un dato che quella regola non viene valutata. Infine correggi la catena spostando una sola regola. Criterio di successo: la dimostrazione è un numero letto dal sistema, non un ragionamento, e dopo la correzione lo stesso numero cambia.
Soluzioni commentate
Esercizio 1. Il risultato misurato è questo, e la prima volta sorprende chiunque:
# nft add rule inet prova input tcp dport 8080 counter accept
# curl -s -o /dev/null -w "codice=%{http_code} " --max-time 2 http://127.0.0.1:8080/
codice=000 exit=28
# nft list chain inet prova input
table inet prova {
chain input {
type filter hook input priority filter; policy drop;
tcp dport 8080 counter packets 3 bytes 180 accept
}
}
La regola ha visto tre pacchetti e li ha accettati, quindi funziona. E la richiesta scade comunque. Il contatore e il codice di uscita sembrano contraddirsi, e non si contraddicono: stanno descrivendo due direzioni diverse dello stesso scambio.
Il ragionamento che scioglie il nodo. Una connessione TCP non è un pacchetto, è un dialogo. La
richiesta del client arriva con porta di destinazione 8080, e la tua regola la copre. La
risposta del server parte dalla porta 8080 e arriva all'ingresso con la porta di
destinazione del client, che è una porta effimera scelta al momento e diversa a ogni connessione.
Quella risposta non corrisponde a nessuna regola, quindi la decide la politica predefinita, che è
drop. Il client aspetta una risposta che il kernel sta buttando.
La correzione non è aggiungere una regola per le porte effimere, che sarebbe aprire sessantamila porte. È dire al firewall di riconoscere il traffico che appartiene a una connessione già autorizzata, cioè il tracciamento di connessione, e va in testa alla catena:
# nft insert rule inet prova input ct state established,related counter accept
# curl -s -o /dev/null -w "codice=%{http_code} " --max-time 2 http://127.0.0.1:8080/
codice=200 exit=0
# nft list chain inet prova input
table inet prova {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related counter packets 12 bytes 4685 accept
tcp dport 8080 counter packets 3 bytes 180 accept
}
}
Dodici pacchetti sulla prima regola, tre sulla seconda: la maggior parte del traffico di una
connessione è traffico di ritorno, e questo spiega da sé perché la regola del tracciamento sta in
cima a tutte le catene scritte da chi sa quello che fa. È anche la riga che ufw genera
per te senza mostrartela, ed è il motivo per cui una catena scritta a mano da zero si comporta
peggio di una generata, finché non sai questa cosa.
Perché le altre spiegazioni non reggono. «Il servizio non era in ascolto» è escluso, perché senza
regole la stessa richiesta risponde 200. «La regola era scritta male» è escluso dal
contatore, che mostra pacchetti maggiori di zero. «Serviva anche una catena di uscita» è escluso
perché la catena di uscita non è stata creata affatto, quindi il traffico in uscita non è filtrato:
il problema è nella direzione di ingresso, sui pacchetti di ritorno.
Esercizio 2. La costruzione più semplice è un drop esplicito sulla
porta, seguito da un accept sulla stessa porta. L'accept è corretto e
inutile. La dimostrazione con un dato è il contatore: con counter su entrambe le regole
e dopo aver generato traffico, il drop mostra i pacchetti contati e l'accept
mostra packets 0 bytes 0. L'ho misurato, e i due valori sono
packets 2 bytes 120 sul primo e packets 0 bytes 0 sul secondo.
Perché le altre dimostrazioni sono più deboli. Mostrare che curl non si connette prova
che il traffico è bloccato, non prova quale regola lo blocca: su una catena di cinque regole
non hai ristretto niente, e l'esercizio precedente ti ha appena mostrato un caso in cui la regola
che corrisponde non è quella che decide l'esito. Rileggere la catena e ragionare sull'ordine è
corretto ma è un ragionamento, e sulle catene generate da ufw o tradotte da
iptables le regole sono decine. Il contatore è un dato che il kernel produce, e risponde
alla domanda «questa regola l'ha vista qualcuno» senza passare da un'interpretazione.
Sulla correzione: spostare una regola in nftables si fa con il suo numero di maniglia, che
nft -a list chain mostra accanto a ogni riga come handle N. È il motivo per
cui conviene elencare con -a prima di modificare: senza la maniglia, l'unico modo di
riordinare è riscrivere la catena, e riscrivere una catena su una macchina remota è esattamente il
gesto che ti chiude fuori.
Che cosa adesso sai fare
Sai distinguere i tre livelli, nft, iptables e ufw, e sai
verificare con un comando quale motore c'è sotto e quale livello sta gestendo le regole di una
macchina. Sai che la prima regola che corrisponde decide, e lo hai provato invertendo due regole
identiche e ottenendo due risultati diversi. Sai distinguere drop da
reject dal lato del client, con il codice di uscita e il tempo di attesa.
Sai leggere il contatore di una regola per stabilire se è stata valutata, invece di riscriverla a tentativi. E sai qual è la sequenza che ti chiude fuori da un server remoto, e perché sembra ragionevole. La verifica concreta: sul prossimo server su cui lavori, prima di toccare qualunque regola, stabilisci quale dei tre livelli la gestisce e scrivi la via di fuga che userai se ti sbagli.