SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor

UFW e IPv6: la porta aperta sul tuo VPS

Su Ubuntu 24.04 UFW gestisce IPv6, ma firewall cloud e regole iptables possono ignorarlo: scopri come verificare le porte esposte e chiuderle sul VPS.

La trappola del firewall IPv6 in una frase

Il firewall protegge IPv4. Il tuo VPS dispone quasi certamente anche di un indirizzo IPv6 pubblico e molti servizi sono in ascolto su IPv6 per impostazione predefinita. Se il firewall copre solo IPv4, oppure se ti affidi a un firewall cloud che filtra soltanto IPv4, tutti questi servizi sono raggiungibili da Internet tramite IPv6, mentre il lato IPv4 sembra protetto. Verifichi una porta con curl, ricevi una connessione rifiutata e ti senti al sicuro. Un attaccante si connette alla stessa porta tramite IPv6 e accede al sistema.

Questa guida mostra da dove deriva questa lacuna su un normale VPS Ubuntu 24.04, come vedere esattamente cosa stai esponendo e come chiudere l'esposizione. UFW non è il problema. In una installazione moderna di Ubuntu, UFW gestisce già IPv6. L'esposizione deriva dai livelli che lo circondano e dai servizi che non sapevi essere in ascolto.

Perché il tuo VPS usa IPv6

Oggi quasi ogni VPS viene fornito con un indirizzo IPv6 pubblico, spesso un intero /64, oltre al relativo indirizzo IPv4. Verifica il tuo:

ip -6 addr show scope global
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
    inet6 2001:db8:2a::1/64 scope global

Il 2001:db8:2a::1 presente in questa guida è un segnaposto di 2001:db8::/32, un prefisso riservato alla documentazione e mai instradato su Internet. L'indirizzo reale visualizzato dal server in quella riga è raggiungibile da qualsiasi punto di Internet, esattamente come il tuo indirizzo IPv4. Ora verifica quali servizi sono in ascolto:

sudo ss -tlnp
State   Recv-Q  Local Address:Port   Process
LISTEN  0       0.0.0.0:22           sshd
LISTEN  0       [::]:22              sshd
LISTEN  0       127.0.0.1:5432       postgres
LISTEN  0       [::]:8080            docker-proxy

Leggi attentamente la colonna Local Address. 0.0.0.0:22 significa "in ascolto su ogni indirizzo IPv4". [::]:22 significa "in ascolto su ogni indirizzo IPv6". 127.0.0.1:5432 è associato all'interfaccia di loopback e non è pubblico, quindi la riga di Postgres è sicura. Le due righe [::] rispondono a tutto Internet tramite IPv6, mentre quella docker-proxy è il tipo di servizio che ci si dimentica di aver avviato.

La maggior parte dei demoni si associa a :: in modo predefinito, perché su Linux un socket :: di norma accetta anche connessioni IPv4. Di conseguenza, la configurazione predefinita di un server appena installato è: "rispondere su entrambi gli stack, ovunque". Il firewall è l'unico componente che si trova davanti a questa esposizione. Per questo un firewall che controlla un solo stack rappresenta un problema reale.

Da dove nasce realmente il divario IPv6

Le cause più comuni sono tre. Su un determinato server può essercene una sola oppure più di una contemporaneamente.

1. Un firewall cloud che filtra soltanto IPv4. Molti firewall dei provider e prodotti basati sui security group sono nati attorno a IPv4 e ignorano IPv6 oppure richiedono regole IPv6 separate, da aggiungere manualmente. Se l'unico firewall disponibile è quello del pannello del provider e non copre IPv6, i servizi [::] sono esposti indipendentemente da ciò che indica per la porta 22 su IPv4. Consultate la documentazione del firewall del provider e cercate esplicitamente la voce IPv6.

2. iptables configurato manualmente senza ip6tables. Il comando iptables interviene soltanto sulle tabelle IPv4. IPv6 utilizza un comando completamente separato, ip6tables, con regole proprie. Se avete scritto uno script firewall pieno di righe iptables -A INPUT ... e non avete mai scritto le corrispondenti regole ip6tables, il firewall IPv6 è vuoto e una catena INPUT vuota con una policy predefinita ACCEPT consente tutto:

sudo ip6tables -L INPUT -n
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Questo output mostra l'intero problema in una sola schermata. IPv4 è filtrato, mentre IPv6 accetta il traffico da qualsiasi origine.

3. Docker pubblica le porte bypassando direttamente il firewall. Quando eseguite docker run -p 8080:80, Docker inserisce le proprie regole prima di quelle di UFW, quindi una porta pubblicata è raggiungibile anche quando ufw status indica che la porta è negata. Le versioni attuali di Docker gestiscono le regole ip6tables per impostazione predefinita; quando IPv6 è abilitato sulla rete del container, lo stesso comportamento vale anche per IPv6. Perché Docker bypassa UFW e come filtrare correttamente le porte dei container spiega il meccanismo e le soluzioni. Consultate le nozioni di base su Docker Compose su un VPS per vedere come vengono dichiarate queste porte pubblicate.

Una nota sull'impostazione IPv6 di UFW. UFW gestisce anche IPv6 e l'impostazione si trova in un unico file. Verificatela:

grep IPV6 /etc/default/ufw

Le versioni moderne di Ubuntu includono IPV6=yes, quindi UFW applica ogni regola a entrambi gli stack. Se vedete IPV6=no, proveniente da un'immagine precedente o da una guida obsoleta, IPv6 non viene lasciato aperto. Con questa impostazione, un UFW attivo blocca tutto il traffico IPv6 ad eccezione dell'interfaccia di loopback; il problema quindi si presenta al contrario: tutto ciò che intendete pubblicare su IPv6 smette di rispondere. Impostatelo su yes se volete che UFW filtri IPv6 usando le vostre regole invece di bloccarlo completamente.

Verifica esattamente cosa stai esponendo

Non fare supposizioni. Misura dall'esterno. Per prima cosa, elenca i socket in ascolto e annota tutti quelli associati a :::

sudo ss -tlnp | grep '::'

Poi, da un'altra macchina, connettiti all'indirizzo IPv6 pubblico del server e prova una porta che ritieni chiusa:

curl -6 -v http://[2001:db8:2a::1]:8080/

Se restituisce una pagina o un banner, la porta è aperta su IPv6. Una porta chiusa restituisce Connection refused oppure va in timeout. Questi due errori non indicano la stessa situazione: la differenza tra connessione rifiutata e timeout mostra se l'host ha risposto rifiutando la connessione oppure se un firewall ha eliminato il pacchetto senza risposta. Per una visione completa, esegui la scansione dell'indirizzo IPv6 con nmap da una macchina esterna al server:

nmap -6 2001:db8:2a::1

Ogni porta che nmap segnala come aperta su IPv6 è raggiungibile da Internet, indipendentemente dal risultato della scansione IPv4. Confrontare affiancate le scansioni IPv4 e IPv6 è il modo più rapido per individuare la differenza: qualsiasi porta aperta su -6 ma chiusa su IPv4 indica un servizio non coperto dal firewall.

Chiudere la falla

Configurare UFW per entrambe le famiglie di indirizzi e impostare il criterio predefinito su deny. Verificare il cambio, quindi impostare una policy predefinita che neghi il traffico in ingresso e consentire soltanto ciò che serve:

sudo sed -i 's/^IPV6=no/IPV6=yes/' /etc/default/ufw
sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw enable
sudo ufw status verbose

Se UFW era già attivo quando è stato modificato IPV6=yes, il cambio diventa effettivo soltanto dopo l'esecuzione di sudo ufw reload.

ufw status elenca ogni regola due volte: una volta senza suffissi e una volta con il suffisso (v6). Quando sono presenti le righe (v6), UFW sta filtrando IPv6:

22/tcp                     ALLOW IN    Anywhere
22/tcp (v6)                ALLOW IN    Anywhere (v6)

Se si gestisce iptables manualmente, replicare ogni regola in ip6tables oppure passare a nftables. Le relative tabelle inet coprono IPv4 e IPv6 in un unico punto ed eliminano interamente questo tipo di errore. Se si scrivono le regole manualmente, una singola tabella di filtro nftables inet è la soluzione più pulita. Se il VPS esegue Rocky o AlmaLinux invece di Ubuntu, non c'è UFW da configurare: si gestisce invece firewalld come front end, e le relative regole di zona si applicano contemporaneamente a entrambe le famiglie di indirizzi.

Associare al loopback i servizi che non devono essere pubblici. Un database, un pannello di amministrazione o un endpoint per le metriche raramente hanno bisogno di un indirizzo pubblico. Associarli a 127.0.0.1 e ::1, in modo che non restino mai in ascolto su un indirizzo instradabile. Per Postgres, impostare listen_addresses = 'localhost'. Per un application server, associarlo a 127.0.0.1 e anteporre un reverse proxy. Chiudere il listener è più efficace che filtrarlo con il firewall, perché in questo modo non c'è nulla da raggiungere.

Non affidarsi a UFW per proteggere le porte pubblicate da Docker. Pubblicare le porte dei container su un indirizzo specifico anziché su tutte le interfacce, ad esempio -p 127.0.0.1:8080:80, così la porta è raggiungibile soltanto dall'host e da ciò a cui si inoltra esplicitamente il traffico tramite proxy. Quando un container deve essere realmente pubblico, collocarlo dietro un reverse proxy Traefik e pubblicare soltanto il proxy, non ogni applicazione.

Aggiungere le regole IPv6 al firewall del provider oppure riconoscere che il firewall del provider non protegge IPv6 e affidare questo compito a UFW o nftables sull'host.

Verifica che la porta sia effettivamente chiusa

Ripeti lo stesso test dall'esterno dopo le modifiche:

curl -6 -v http://[2001:db8:2a::1]:8080/
nmap -6 2001:db8:2a::1

La porta che prima rispondeva dovrebbe ora rifiutare la connessione o andare in timeout, e nmap dovrebbe indicarla come filtrata o chiusa. Se una porta è ancora aperta, verifica nuovamente le tre origini indicate sopra: un servizio ancora in ascolto su :: senza alcuna regola davanti, una regola Docker applicata prima di UFW oppure un firewall del provider che non ha mai gestito IPv6.

È ancora più sicuro mantenere completamente fuori da Internet i servizi sensibili. Metti SSH e i pannelli di amministrazione dietro una VPN WireGuard e configura il firewall in modo che le relative porte rispondano soltanto attraverso il tunnel. In questo modo la questione dell'esposizione IPv6 non si applica più a questi servizi. Per rallentare le scansioni brute-force dirette contro ciò che resta pubblico, aggiungi Fail2ban davanti a SSH a un firewall con criterio predefinito di rifiuto.

Se le porte sono un concetto nuovo, cosa sono le porte e come ascoltano i servizi è la guida introduttiva da leggere per prima.

FAQ

UFW blocca IPv6 per impostazione predefinita?

Su un'installazione moderna di Ubuntu 24.04, sì. UFW legge IPV6=yes da /etc/default/ufw e applica ogni regola sia a IPv4 sia a IPv6; ufw status mostra le regole IPv6 con il suffisso (v6). Il problema si presenta quando ci si affida a un firewall del provider che filtra solo IPv4, quando le regole iptables scritte manualmente non hanno una controparte ip6tables oppure quando Docker pubblica una porta aggirando UFW. IPV6=no (proveniente da un'immagine precedente o da un tutorial datato) non apre IPv6: con questa impostazione, un UFW attivo scarta tutto il traffico IPv6 tranne quello di loopback. Verificare l'impostazione con grep IPV6 /etc/default/ufw.

Come posso verificare cosa espone il mio VPS su IPv6?

Eseguire sudo ss -tlnp e annotare ogni listener il cui indirizzo locale inizia con [::], perché questo indica che risponde su ogni interfaccia IPv6. Quindi, da un'altra macchina, testare direttamente l'indirizzo IPv6 pubblico del server con curl -6 -v http://[YOUR:IPV6::ADDR]:PORT/ oppure analizzarlo con nmap -6 YOUR:IPV6::ADDR. Qualsiasi porta aperta nella scansione IPv6 ma chiusa su IPv4 costituisce una lacuna.

Perché posso raggiungere la porta del container Docker anche se UFW indica che è bloccata?

Quando si pubblica una porta con -p, Docker inserisce le proprie regole firewall prima di quelle di UFW. Di conseguenza, la porta pubblicata è raggiungibile anche se ufw status la elenca come negata. Questo avviene su IPv4 e anche su IPv6 quando il supporto IPv6 di Docker è attivo. Pubblicare la porta su un indirizzo specifico, ad esempio -p 127.0.0.1:8080:80, oppure mettere il container dietro un reverse proxy e pubblicare solo il proxy.

Serve comunque un firewall IPv6 se il mio firewall IPv4 è configurato correttamente?

Sì. IPv4 e IPv6 sono stack di rete separati, con regole firewall separate. Un insieme di regole IPv4 perfettamente configurato non ha alcun effetto sul traffico IPv6. Se il VPS dispone di un indirizzo IPv6 pubblico, come avviene nella maggior parte dei casi, qualsiasi servizio in ascolto su :: rimane raggiungibile tramite IPv6 finché una regola firewall IPv6 o un binding su loopback non ne interrompe l'accesso.

Come posso fare in modo che un servizio ascolti solo su IPv4 o solo su localhost?

Impostare l'indirizzo di binding del servizio nella relativa configurazione. Usare 127.0.0.1 per il solo loopback IPv4 oppure 0.0.0.0 per tutti gli indirizzi IPv4 senza un listener IPv6. Postgres usa listen_addresses, SSH usa ListenAddress e la maggior parte dei server applicativi espone un parametro host o un flag bind. Confermare il risultato con sudo ss -tlnp e verificare che Local Address non mostri più [::].