SSD Nodes Learn
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-07-24

Docker bypassa UFW: perché e come risolvere

Scopri perché le regole UFW non bloccano le porte Docker. Impara a configurare correttamente iptables e le impostazioni di Docker per una sicurezza totale.

Perché Docker bypassa UFW

Docker bypassa UFW perché le porte dei container pubblicate non passano mai attraverso le regole gestite da UFW. Quando si esegue docker run -p 8080:80, Docker scrive una regola DNAT (destination network address translation) nella catena PREROUTING della tabella nat del kernel. Tale regola riscrive la destinazione di ogni pacchetto verso l'indirizzo privato del container prima che il kernel determini la destinazione finale. Il pacchetto riscritto viene quindi inoltrato nel container tramite la catena FORWARD, controllata da Docker. Le regole di UFW risiedono nella catena INPUT e il pacchetto non vi entra mai. Di conseguenza, ufw status mostra il blocco predefinito, sudo ufw deny 8080 riporta il successo, ma la porta 8080 risponde ancora a tutta la rete internet.

Questo non è un bug di Docker e UFW non è guasto. Entrambi gli strumenti programmano lo stesso firewall del kernel. Le regole di Docker agiscono in una fase precedente del percorso del pacchetto, quindi UFW non viene mai interpellato. Questa guida dimostra il bypass, spiega il meccanismo e illustra le due soluzioni efficaci: pubblicare le porte su 127.0.0.1 e filtrare nella catena DOCKER-USER. Se non si ha esperienza con UFW, configurarlo prima utilizzando la guida ai concetti base di UFW, poiché un firewall con blocco predefinito rimane la base corretta per tutto il resto sul server.

Verifica il bypass sul tuo server

Inizia da un VPS con UFW attivo e una policy di default deny per il traffico in entrata. Avvia un container web con una porta pubblicizzata:

sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpine

ufw status verbose mostra Default: deny (incoming), allow (outgoing) e nessuna regola per la porta 8080. Secondo il report del firewall, la porta è chiusa. Ora esegui il test da una macchina diversa, non dal server stesso:

curl -I http://your-vps-ip:8080/
HTTP/1.1 200 OK

Il container risponde. Aggiungi una regola di deny esplicita e testa di nuovo:

sudo ufw deny 8080/tcp

La porta risponde ancora, perché la regola di deny si trova in una chain che il pacchetto non visita mai. UFW non ha fallito. Non è mai stato consultato. Questo è il motivo per cui il problema è difficile da individuare: non viene stampato alcun errore, il deploy funziona e l'output dello stato del firewall appare esattamente come quello di un server sicuro e bloccato.

Il meccanismo: PREROUTING viene eseguito prima di INPUT

Il kernel elabora i pacchetti in entrata secondo un ordine fisso; il problema deriva proprio da questo ordine.

  1. PREROUTING viene eseguito per primo. Le regole in questa fase possono riscrivere la destinazione del pacchetto; la regola di Docker per una porta pubblicata esegue esattamente questa operazione.
  2. Successivamente avviene la decisione di routing. Un pacchetto indirizzato all'host stesso viene inviato alla catena INPUT. Un pacchetto indirizzato a qualsiasi altra macchina viene inviato alla catena FORWARD.
  3. Le regole di UFW si trovano in INPUT. Le regole di Docker si trovano in FORWARD.

Esamina la regola di Docker per il container appena avviato:

sudo iptables -t nat -L DOCKER -n
Chain DOCKER (2 references)
target   prot opt source       destination
RETURN   0    --  0.0.0.0/0    0.0.0.0/0
DNAT     6    --  0.0.0.0/0    0.0.0.0/0    tcp dpt:8080 to:172.17.0.2:80

La riga DNAT spiega l'intero problema. Ogni pacchetto che arriva sulla porta 8080 vede la propria destinazione riscrita in 172.17.0.2:80, ovvero l'indirizzo del container sulla rete bridge privata di Docker. Dopo la riscrittura, il pacchetto non è più indirizzato all'host; la decisione di routing lo invia quindi lungo il percorso FORWARD, dove Docker ha già aggiunto regole per accettare il traffico nelle proprie reti. La tua regola deny 8080/tcp attende in INPUT un pacchetto che non arriverà mai.

Su Ubuntu 24.04 il comando iptables è un'interfaccia per nftables, ma l'ordine delle catene e il risultato sono identici. Sia UFW che Docker scrivono nello stesso pipeline dei pacchetti del kernel, ma il punto di ingresso di Docker avviene prima.

La soluzione quotidiana: pubblicare le porte su 127.0.0.1

La maggior parte dei container non deve essere pubblica. Un database, un server applicativo dietro un reverse proxy, un pannello di amministrazione o un endpoint per le metriche non devono rispondere direttamente a Internet. Pubblicali sull'indirizzo loopback:

docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpine

Oppure in un file Compose:

services:
  web:
    image: nginx:1.29-alpine
    ports:
      - "127.0.0.1:8080:80"

Questo metodo funziona perché la regola DNAT di Docker ora corrisponde solo ai pacchetti indirizzati a 127.0.0.1. Un pacchetto proveniente da Internet non può avere legittimamente tale destinazione; pertanto, il kernel lo scarta prima dell'esecuzione di qualsiasi regola del firewall. La porta è raggiungibile dall'host e da nessun altro dispositivo. Verifica il binding:

sudo ss -tlnp | grep 8080

L'output deve mostrare 127.0.0.1:8080, non 0.0.0.0:8080 o [::]:8080. Conferma quindi da un'altra macchina che curl http://your-vps-ip:8080/ viene rifiutata.

Per i servizi che devono essere esposti su Internet, utilizza un unico reverse proxy che gestisca le porte 80 e 443 e instradi il traffico tramite hostname; non pubblicare nient'altro. Questo è il modello descritto nella guida al reverse proxy Traefik ed è il modo in cui un'app self-hosted come Nextcloud su un VPS rimane inaccessibile se non tramite il proxy. La dichiarazione delle voci ports: e il resto del workflow Compose sono trattati nella guida ai concetti base di Docker Compose.

Con ogni container interno configurato su loopback, UFW torna a svolgere il suo compito standard: proteggere le porte gestite dall'host. Configura il set di regole qui, quindi esegui i comandi in ordine:

ToolUFW rule generator

Real filtering: la chain DOCKER-USER

A volte una porta del container deve rimanere pubblicata sulla rete ma deve essere limitata. Ad esempio, una porta di una replica del database che può essere raggiunta solo da un indirizzo specifico dell'ufficio. Per questo, Docker fornisce la chain DOCKER-USER. Ogni pacchetto diretto a un container passa attraverso DOCKER-USER prima delle regole accept di Docker, e Docker non scrive mai regole in questa chain. La chain è a disposizione dell'utente e Docker non modifica il suo contenuto durante i riavvii del daemon.

Un avvertimento prima del comando: quando un pacchetto raggiunge DOCKER-USER, la riscrittura DNAT è già avvenuta. La porta di destinazione del pacchetto è la porta del container (80 nell'esempio), non la porta pubblicata (8080). Una regola che intercetta --dport 8080 non troverà corrispondenze. Il metodo affidabile consiste nell'intercettare la porta originariamente contattata dal client, che il connection tracker del kernel ricorda:

sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP

Interpretazione: per i pacchetti entrati su eth0 che appartengono a una connessione la cui porta di destinazione originale era 8080, scarta tutto ciò che non proviene da 10.0.0.10. Il match --ctdir ORIGINAL limita la regola alla direzione client-container, evitando di bloccare per errore i pacchetti di risposta. Sostituisci eth0 con la tua interfaccia pubblica; ip route | grep default la identifica. Testa il comando come precedentemente: curl dall'indirizzo consentito ha successo, mentre da qualsiasi altro indirizzo la connessione va in timeout.

Le regole aggiunte con il comando iptables scompaiono al riavvio. Poiché UFW gestisce già questo firewall, il luogo corretto per renderle persistenti è /etc/ufw/after.rules. Aggiungi un blocco alla fine del file:

*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP
COMMIT

Esegui quindi sudo ufw reload. UFW riapplica quel file ad ogni reload e ad ogni avvio; il filtraggio dei container è ora gestito nello stesso punto del resto del firewall e sopravvive sia al riavvio che all'aggiornamento di Docker.

Perché non disabilitare l'integrazione iptables di Docker

Le risposte precedenti a questo problema suggeriscono di impostare { "iptables": false } in /etc/docker/daemon.json. Non farlo. Le regole del firewall di Docker svolgono funzioni superiori alla semplice pubblicazione delle porte. La regola di masquerade permette ai container di accedere a Internet verso l'esterno tramite l'indirizzo dell'host; se l'integrazione è disattivata, i container non possono scaricare immagini, raggiungere i mirror dei pacchetti o chiamare API esterne. Le regole DNAT sono necessarie per il funzionamento di -p; senza di esse, le porte pubblicate smettono di funzionare completamente. Anche le regole di isolamento che separano le reti Compose diventano inesistenti. Disabilitando l'integrazione si risolverebbe il bypass, ma si comprometterebbe il networking dei container. Dovresti scrivere e gestire manualmente ogni singola di quelle regole. La documentazione ufficiale di Docker descrive questa impostazione come una funzione per utenti che intendono procedere esattamente in questo modo. La catena DOCKER-USER esiste proprio per evitare la necessità di questo switch.

Il lato IPv6 dello stesso problema

Verificare innanzitutto l'aspetto della porta pubblicata su IPv6:

sudo ss -tlnp | grep 8080

A partire da Docker Engine 27, Docker gestisce ip6tables di default. Su una rete Docker con IPv6 abilitato, una porta pubblicata riceve lo stesso trattamento DNAT nelle tabelle IPv6; pertanto esiste lo stesso bypass e si applica la stessa soluzione: la chain DOCKER-USER esiste anche in ip6tables, quindi replicare la regola con sudo ip6tables -I DOCKER-USER ... e testare dall'esterno con curl verso l'indirizzo IPv6 pubblico del server, ad esempio curl -6 http://[2001:db8:2a::1]:8080/.

Su una rete senza IPv6, i client IPv6 sono gestiti invece da docker-proxy, un normale processo user-space che ascolta su [::]:8080 e inoltra il traffico nel container tramite IPv4. Il traffico verso un processo host passa attraverso INPUT, quindi UFW può filtrare quel percorso, ma solo se UFW sta gestendo IPv6. Se ciò avvenga, e le altre modalità con cui si crea una falla IPv6 su un VPS, è l'argomento di la guida UFW e IPv6.

Pubblicare su loopback evita il problema: -p 127.0.0.1:8080:80 effettua il bind solo sul loopback IPv4, quindi non esiste alcun listener IPv6 e nulla è raggiungibile dall'esterno su nessuno dei due stack.

Il pattern che garantisce la sicurezza

  • Pubblica ogni porta interna su 127.0.0.1, in modo da non esporla mai direttamente.
  • Assegna l'accesso pubblico a un unico reverse proxy che gestisce le porte 80 e 443.
  • Mantieni la configurazione UFW su deny di default per l'host, consentendo solo SSH e le porte del proxy.
  • Filtra le porte dei container effettivamente pubbliche in DOCKER-USER, basandoti sulla porta di destinazione originale memorizzata in /etc/ufw/after.rules.
  • Mantieni attiva l'integrazione iptables di Docker.

Configurata una volta, questa procedura elimina gli imprevisti: ufw status descrive l'host e DOCKER-USER descrive i container. Nulla viene pubblicato per errore, e la prossima volta che digiti docker run -p esporrai esattamente ciò che intendevi.

FAQ

Perché riesco a raggiungere il mio container Docker se UFW blocca la porta?

Il problema accade perché Docker pubblica la porta tramite una regola DNAT nella chain PREROUTING. Questa regola riscrive la destinazione del pacchetto verso l'indirizzo del container prima di qualsiasi filtraggio. Il pacchetto segue quindi il percorso FORWARD, mentre le regole di UFW si trovano nella chain INPUT, una chain che il pacchetto non attraversa mai. Il firewall non viene consultato e le regole di blocco non hanno effetto sulle porte pubblicate dei container.

Come posso fare in modo che UFW blocchi le porte pubblicate da Docker?

UFW non può farlo direttamente perché le sue regole si trovano nella chain errata. È possibile smettere di esporre la porta pubblicandola come 127.0.0.1:8080:80 per consentire l'accesso solo all'host, oppure filtrare nella chain DOCKER-USER utilizzando una regola iptables che identifichi la porta di destinazione originale tramite conntrack. È necessario rendere persistente tale regola in /etc/ufw/after.rules affinché sopravviva ai riavvii e a ufw reload.

Devo impostare "iptables": false nel file daemon.json di Docker?

No. Questa impostazione rimuove tutte le regole di firewall e NAT di Docker, causando problemi superiori al semplice bypass. I container perdono l'accesso a Internet in uscita perché la regola di masquerade viene rimossa, e le porte pubblicate smettono di funzionare perché mancano le regole DNAT. Utilizzare invece la pubblicazione su loopback e la chain DOCKER-USER; queste soluzioni risolvono l'esposizione senza interrompere il networking dei container.

Docker bypassa UFW anche su IPv6?

Su Docker Engine 27 e versioni successive, la gestione di ip6tables è attiva di default. Una porta pubblicata su una rete Docker con IPv6 viene riscritta bypassando UFW esattamente come su IPv4, e richiede la stessa regola DOCKER-USER replicata con ip6tables. Sulle reti senza IPv6, il processo docker-proxy ascolta su [::] e quel traffico passa attraverso INPUT, dove UFW può filtrarlo se gestisce IPv6. Pubblicare su 127.0.0.1 evita entrambi i casi, poiché non viene attivato alcun ascolto su IPv6.