SSD Nodes Learn
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-07-24

Docker omzeilt UFW: oorzaak en oplossing

Docker schrijft DNAT regels in de iptables chain waardoor UFW regels worden genegeerd. Leer hoe u dit oplost via de DOCKER chain of door poorten te beperken.

Waarom Docker UFW omzeilt

Docker omzeilt UFW omdat gepubliceerde containerports nooit door de firewallregels van UFW gaan. Wanneer u docker run -p 8080:80 uitvoert, schrijft Docker een DNAT (destination network address translation) regel in de PREROUTING chain van de nat table van de kernel. Die regel herschrijft het doeladres van elk pakket naar het private adres van de container voordat de kernel bepaalt waar het pakket naartoe moet. Het herschreven pakket wordt vervolgens doorgestuurd naar de container via de FORWARD chain, die door Docker wordt beheerd. De regels van UFW staan in de INPUT chain en het pakket komt daar nooit in terecht. Daarom toont ufw status een standaard blokkade, rapporteert sudo ufw deny 8080 succes, en reageert poort 8080 nog steeds op het hele internet.

Dit is geen fout in Docker en UFW werkt correct. Beide tools programmeren dezelfde kernel firewall. De regels van Docker treden simpelweg op een eerder punt in het pad van het pakket in, waardoor UFW nooit wordt geraadpleegd. Deze handleiding demonstreert de omzeiling, legt het mechanisme uit en behandelt de twee werkende oplossingen: poorten publiceren op 127.0.0.1 en filteren in de DOCKER-USER chain. Als UFW nieuw voor u is, gebruik dan eerst de UFW firewall basics guide, omdat een firewall met standaard blokkade de juiste basis blijft voor de rest van de server.

Test de bypass op uw eigen server

Begin op een VPS waar UFW actief is met een standaard deny-policy voor inkomend verkeer. Start een webcontainer met een gepubliceerde port:

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

ufw status verbose toont Default: deny (incoming), allow (outgoing) en geen regel voor port 8080. Volgens de rapportage van de firewall is de port gesloten. Test nu vanaf een andere machine, niet vanaf de server zelf:

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

De container reageert. Voeg een expliciete deny-regel toe en test opnieuw:

sudo ufw deny 8080/tcp

De port reageert nog steeds, omdat de deny-regel in een chain staat die het pakket nooit bereikt. UFW is niet gefaald. Het is nooit geraadpleegd. Dit is ook de reden waarom het probleem zo goed verborgen blijft: er wordt nergens een foutmelding geprint, de deployment werkt en de output van de firewall-status ziet er precies uit als een gezonde, beveiligde server.

Het mechanisme: PREROUTING draait voor INPUT

De kernel verwerkt inkomende pakketten in een vaste volgorde. Het probleem ontstaat door deze volgorde.

  1. PREROUTING draait als eerste. Regels in deze chain kunnen de bestemming van een pakket wijzigen. De Docker-regel voor een gepubliceerde port doet precies dit.
  2. Daarna volgt de routing-beslissing. Een pakket dat gericht is aan de host zelf gaat naar de INPUT chain. Een pakket dat gericht is aan een andere machine gaat naar de FORWARD chain.
  3. De regels van UFW staan in INPUT. De regels van Docker staan in FORWARD.

Bekijk de Docker-regel voor de container die u zojuist heeft gestart:

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

De DNAT regel is de kern van het probleem. Elk pakket dat binnenkomt voor port 8080 krijgt een nieuwe bestemming: 172.17.0.2:80, het adres van de container op het private bridge-netwerk van Docker. Na deze wijziging is het pakket niet langer gericht aan de host. De routing-beslissing stuurt het pakket daarom naar het FORWARD pad. Docker heeft daar al regels toegevoegd die verkeer naar de eigen netwerken accepteren. Uw deny 8080/tcp regel wacht in INPUT op een pakket dat nooit aankomt.

Op Ubuntu 24.04 is het iptables commando een front-end voor nftables, maar de volgorde van de chains en het resultaat zijn identiek. Zowel UFW als Docker schrijven naar dezelfde kernel packet pipeline, waarbij het instappunt van Docker eerder in de volgorde ligt.

De standaardoplossing: publiceer poorten op 127.0.0.1

De meeste containers hoeven niet publiek toegankelijk te zijn. Een database, een app-server achter een reverse proxy, een admin-paneel of een metrics-endpoint: geen van deze componenten hoeft direct via het internet bereikbaar te zijn. Publiceer deze op het loopback-adres:

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

Of in een Compose-bestand:

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

Dit werkt omdat de DNAT-regel van Docker nu alleen pakketten matcht die gericht zijn op 127.0.0.1. Een pakket van het internet kan nooit legitiem dit doeladres hebben, waardoor de kernel het pakket verwijdert voordat een firewall-regel wordt uitgevoerd. De poort is bereikbaar vanaf de host, en verder nergens vanaf. Controleer de binding:

sudo ss -tlnp | grep 8080

U wilt 127.0.0.1:8080 in de output zien, niet 0.0.0.0:8080 of [::]:8080. Controleer vervolgens vanaf een andere machine of curl http://your-vps-ip:8080/ wordt geweigerd.

Voor services die wel via het internet bereikbaar moeten zijn, gebruikt u één reverse proxy die poort 80 en 443 bezit en routeert op basis van de hostname. Publiceer verder niets. Dit is het patroon dat de Traefik reverse proxy guide beschrijft. Dit is ook hoe een zelfgehoste applicatie zoals Nextcloud on a VPS onbereikbaar blijft, behalve via de proxy. Hoe ports:-vermeldingen worden gedefinieerd en de rest van de Compose-workflow, wordt uitgelegd in the Docker Compose basics guide.

Wanneer elke interne container op loopback draait, kan UFW zijn normale taak uitvoeren: het bewaken van de poorten die de host zelf serveert. Stel de regels hier op en voer de commando's in deze volgorde uit:

ToolUFW rule generator

Real filtering: de DOCKER-USER chain

Soms moet een containerport gepubliceerd blijven op het netwerk, maar moet deze beperkt worden. Denk aan een database-replica port die alleen bereikbaar mag zijn via één specifiek kantooradres. Hiervoor biedt Docker de DOCKER-USER chain. Elk pakket dat naar een container gaat, passeert DOCKER-USER voordat de eigen accept-rules van Docker worden toegepast. Docker schrijft zelf nooit regels in deze chain. De chain is bedoeld voor eigen gebruik en Docker laat de inhoud ervan ongewijzigd tijdens het herstarten van de daemon.

Een belangrijke waarschuwing voor het commando: tegen de tijd dat een pakket DOCKER-USER bereikt, heeft de DNAT-rewrite al plaatsgevonden. De destination port van het pakket is de containerport (80 in ons voorbeeld), niet de gepubliceerde port (8080). Een regel die op --dport 8080 matcht, zal daarom niets vinden. De betrouwbare methode is om te matchen op de port die de client oorspronkelijk heeft aangeroepen; de connection tracker van de kernel onthoudt dit:

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

Dit betekent: voor pakketten die binnenkwamen op eth0 en horen bij een verbinding waarvan de oorspronkelijke destination port 8080 was, wordt alles gedropt wat niet van 10.0.0.10 komt. De --ctdir ORIGINAL match beperkt de regel tot de client-naar-container richting, zodat antwoordpakketten niet per ongeluk worden geblokkeerd. Vervang eth0 door uw publieke interface; ip route | grep default geeft de naam aan. Test dit op dezelfde manier als eerder: curl vanaf het toegestane adres slaagt, en vanaf de rest van het internet loopt de verbinding vast (timeout).

Regels toegevoegd met het iptables commando verdwijnen na een reboot. Omdat UFW dit firewall al beheert, is de juiste plek om deze regels permanent te maken /etc/ufw/after.rules. Voeg een blok toe aan het einde van het bestand:

*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

Voer daarna sudo ufw reload uit. UFW verwerkt dat bestand bij elke reload en elke boot. Uw container filtering staat nu op dezelfde plek als de rest van uw firewall en blijft behouden na een reboot of een Docker upgrade.

Waarom u de iptables-integratie van Docker niet moet uitschakelen

Oudere antwoorden op dit probleem adviseren om { "iptables": false } in /etc/docker/daemon.json in te stellen. Doe dit niet. De firewall-regels van Docker doen veel meer dan alleen poorten publiceren. De masquerade-regel zorgt ervoor dat containers via het adres van de host toegang krijgen tot het internet. Als de integratie is uitgeschakeld, kunnen containers geen images ophalen, geen package mirrors bereiken en geen externe API's aanroepen. De DNAT-regels zijn noodzakelijk voor de werking van -p; zonder deze regels stoppen de gepubliceerde poorten volledig met werken. Ook de isolatieregels die verschillende Compose-netwerken gescheiden houden, vervallen dan. U lost het probleem van de bypass op door de netwerkverbinding van de containers te verbreken. U moet dan elke regel handmatig schrijven en onderhouden. De documentatie van Docker beschrijft deze instelling als een optie voor gebruikers die dit precies willen doen. De DOCKER-USER chain bestaat juist zodat deze schakelaar niet nodig is.

De IPv6-zijde van hetzelfde probleem

Controleer eerst hoe de gepubliceerde port eruitziet op IPv6:

sudo ss -tlnp | grep 8080

Sinds Docker Engine 27 beheert Docker ip6tables standaard. Op een Docker-netwerk met ingeschakelde IPv6 krijgt een gepubliceerde port dezelfde DNAT-behandeling in de IPv6-tables. Hierdoor bestaat daar dezelfde bypass en geldt dezelfde oplossing: de DOCKER-USER chain bestaat ook in ip6tables. Spiegel uw regel met sudo ip6tables -I DOCKER-USER ... en test vanaf buiten met curl tegen het publieke IPv6-adres van uw server, bijvoorbeeld curl -6 http://[2001:db8:2a::1]:8080/.

Op een netwerk zonder IPv6 worden IPv6-clients in plaats daarvan afgehandeld door docker-proxy. Dit is een normaal user-space proces dat luistert op [::]:8080 en het verkeer via IPv4 naar de container doorstuurt. Verkeer naar een host-proces gaat wel via INPUT, dus UFW kan dat pad filteren, maar alleen wanneer UFW IPv6 beheert. Of dit het geval is, en andere manieren waarop een IPv6-lek ontstaat op een VPS, is het onderwerp van de UFW en IPv6 gids.

Publiceren op loopback omzeilt de gehele kwestie: -p 127.0.0.1:8080:80 bindt alleen aan de IPv4 loopback, waardoor er geen IPv6-listener is en er niets van buitenaf bereikbaar is op beide stacks.

Het werkende patroon

  • Publiceer elke interne port op 127.0.0.1, zodat deze nooit direct blootgesteld wordt.
  • Wijs de publieke zijde toe aan één reverse proxy die de ports 80 en 443 beheert.
  • Gebruik UFW met de standaardinstelling deny voor de host, waarbij alleen SSH en de proxy ports worden toegestaan.
  • Filter echt publieke container ports in DOCKER-USER, gebaseerd op de oorspronkelijke destination port, opgeslagen in /etc/ufw/after.rules.
  • Laat de iptables-integratie van Docker aanstaan.

Eenmalig ingesteld, voorkomt dit verrassingen: ufw status beschrijft de host, en DOCKER-USER beschrijft de containers. Niets wordt per ongeluk gepubliceerd, en de volgende docker run -p die u typt, legt precies uit wat u bedoelde.

FAQ

Waarom kan ik mijn Docker container bereiken terwijl UFW de poort blokkeert?

Dit komt doordat Docker de poort publiceert met een DNAT-regel in de PREROUTING chain. Deze regel herschrijft het doeladres van het pakket naar het adres van de container voordat er filtering plaatsvindt. Het pakket volgt daarna het FORWARD pad. De regels van UFW staan in de INPUT chain, een chain waar het pakket nooit in komt. De firewall wordt niet geraadpleegd, waardoor de deny-regels geen effect hebben op gepubliceerde poorten van containers.

Hoe kan ik UFW laten blokkeren wat Docker publiceert?

UFW kan dit niet zelf, omdat de regels in de verkeerde chain staan. Stop met het exposen van de poort door deze als 127.0.0.1:8080:80 te publiceren, zodat alleen de host deze kan bereiken. Of filter in de DOCKER-USER chain met een iptables-regel die het oorspronkelijke doelpoort-adres via conntrack matcht. Maak deze regel persistent in /etc/ufw/after.rules zodat deze blijft bestaan na reboots en ufw reload.

Moet ik "iptables": false instellen in Docker's daemon.json?

Nee. Deze instelling verwijdert alle firewall- en NAT-regels van Docker. Dit veroorzaakt meer problemen dan enkel het omzeilen van de firewall. Containers verliezen de toegang tot het internet omdat de masquerade-regel ontbreekt. Gepubliceerde poorten werken niet meer omdat de DNAT-regels ontbreken. Gebruik in plaats daarvan loopback-publicatie en de DOCKER-USER chain; dit lost het blootstellen op zonder de netwerkverbinding van de container te verbreken.

Omzeilt Docker ook UFW op IPv6?

Op Docker Engine 27 en later staat ip6tables-beheer standaard aan. Een poort die is gepubliceerd op een IPv6-netwerk wordt precies zoals bij IPv4 om de UFW heen herschreven. Hiervoor is een gespiegelde DOCKER-USER regel met ip6tables nodig. Op netwerken zonder IPv6 luistert het docker-proxy proces op [::]. Die verkeersstroom gaat wel door INPUT, waar UFW het kan filteren als UFW IPv6 beheert. Publiceren op 127.0.0.1 voorkomt beide scenario's, omdat er dan helemaal niets op IPv6 luistert.