Skip to main content

Module 3 · Networking

Firewall with iptables/ufw

  • Lesson 8/15
  • 40 min
  • Difficulty: Intermediate

This lesson is not available in English yet: you are reading it in Italian.

Objective

Stabilire quale livello gestisce le regole di una macchina, prevedere quale regola decide usando l'ordine di valutazione, e leggere il contatore di una regola per sapere se e' stata valutata

Prerequisites

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.