SSD Nodes Learn Hosting plans →
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-28

Docker omzeilt UFW: zo beveiligt u uw containers

Docker omzeilt UFW door iptables regels die de firewall overslaan. Leer waarom uw poorten openstaan en hoe u dit definitief oplost door de DOCKER-USER chain correct te configureren.

Waarom Docker UFW omzeilt

Docker omzeilt UFW omdat gepubliceerde containerpoorten nooit de firewallregels passeren die UFW beheert. Wanneer u docker run -p 8080:80 uitvoert, schrijft Docker een DNAT-regel (destination network address translation) naar de PREROUTING-chain van de nat-tabel in de kernel. Die regel herschrijft de bestemming van elk pakket naar het privéadres van de container voordat de kernel beslist waar het pakket naartoe gaat. Het herschreven pakket wordt vervolgens doorgestuurd naar de container via de FORWARD-chain, die door Docker wordt beheerd. De regels van UFW bevinden zich in de INPUT-chain, en het pakket komt daar nooit in terecht. Hierdoor toont ufw status een standaard weigering, rapporteert sudo ufw deny 8080 succes, en antwoordt poort 8080 nog steeds op het gehele internet.

Dit is geen bug in Docker en UFW is niet defect. Beide tools programmeren dezelfde kernel-firewall. De regels van Docker werken simpelweg op een eerder punt in het pad van het pakket, waardoor UFW nooit wordt geraadpleegd. Deze handleiding demonstreert de omzeiling, legt het mechanisme uit en behandelt vervolgens de twee werkende oplossingen: poorten publiceren op 127.0.0.1 en filteren in de DOCKER-USER-chain. Als UFW nieuw voor u is, stel het dan eerst in met de basisgids voor de UFW-firewall, omdat een firewall met standaard weigering nog steeds de juiste basis vormt voor al het andere op de server.

De bypass op uw eigen server bekijken

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

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 poort 8080. Volgens het eigen rapport van de firewall is de poort gesloten. Test dit nu vanaf een andere machine, niet vanaf de server zelf:

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

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

sudo ufw deny 8080/tcp

De poort antwoordt nog steeds, omdat de 'deny'-regel zich in een chain bevindt die het pakket nooit bereikt. UFW heeft niet gefaald. De firewall werd simpelweg niet geraadpleegd. Dit is ook de reden waarom het probleem zo goed verborgen blijft: er wordt nergens een foutmelding getoond, de implementatie werkt en de status van de firewall ziet eruit als die van een correct beveiligde server.

Het mechanisme: PREROUTING wordt uitgevoerd vóór INPUT

De kernel verwerkt een inkomend pakket in een vaste volgorde, en het hele probleem ligt in die volgorde.

  1. PREROUTING wordt als eerste uitgevoerd. Regels hier kunnen de bestemming van het pakket herschrijven, en de regel van Docker voor een gepubliceerde poort doet precies dat.
  2. De routeringsbeslissing volgt daarna. Een pakket dat aan de host zelf is geadresseerd, gaat naar de INPUT-chain. Een pakket dat aan een andere machine is geadresseerd, gaat naar de FORWARD-chain.
  3. De regels van UFW bevinden zich in INPUT. De regels van Docker bevinden zich in FORWARD.

Bekijk de regel van Docker voor de container die u zojuist hebt 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 regel DNAT vertelt het hele verhaal. Elk pakket dat binnenkomt voor poort 8080 krijgt zijn bestemming herschreven naar 172.17.0.2:80, het adres van de container op het private bridge-netwerk van Docker. Na de herschrijving is het pakket niet langer geadresseerd aan de host, dus de routeringsbeslissing stuurt het via het FORWARD-pad, waar Docker al regels heeft toegevoegd die verkeer naar zijn 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 de uitkomst zijn identiek. UFW en Docker schrijven beide naar dezelfde packet pipeline van de kernel, en het toegangspunt van Docker ligt eerder. Niets hiervan is specifiek voor UFW: firewalld op een Rocky of AlmaLinux VPS filtert op hetzelfde punt in die pipeline en wordt omzeild door dezelfde DNAT-regel, dus de onderstaande oplossingen zijn daar ook van toepassing.

De dagelijkse oplossing: poorten publiceren op 127.0.0.1

De meeste containers hoefden in de eerste plaats nooit publiek toegankelijk te zijn. Een database, een applicatieserver achter een reverse proxy, een beheerpaneel of een metrics-endpoint: geen van deze hoort direct op het internet te reageren. Publiceer ze 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 overeenkomt met pakketten die gericht zijn aan 127.0.0.1. Een pakket vanaf het internet kan nooit legitiem die bestemmingslocatie hebben, waardoor de kernel het pakket verwijdert voordat enige firewallregel wordt uitgevoerd. De poort is bereikbaar vanaf de host, en vanaf niets anders. Controleer de binding:

sudo ss -tlnp | grep 8080

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

Voor de services die wel aan het internet moeten worden blootgesteld, draait u één reverse proxy die poort 80 en 443 beheert en routeert op basis van hostnaam; publiceer verder niets. Dat is het patroon dat de Traefik reverse proxy handleiding opbouwt, en het is de manier waarop een zelfgehoste applicatie zoals Nextcloud op een VPS onbereikbaar blijft, behalve via de proxy. Hoe ports:-vermeldingen worden gedeclareerd, en de rest van de Compose-workflow, wordt behandeld in de Docker Compose basisgids.

Nu elke interne container op loopback draait, doet UFW weer zijn normale werk: het bewaken van de poorten die de host zelf aanbiedt. Bouw die regelset hier op en voer vervolgens de commando's in volgorde uit:

ToolUFW rule generator

Realistische filtering: de DOCKER-USER chain

Soms moet een containerpoort gepubliceerd blijven naar het netwerk, maar wel beperkt worden. Denk aan een database-replicatiepoort die alleen bereikbaar mag zijn vanaf één kantooradres. Docker biedt hiervoor de DOCKER-USER chain. Elk pakket dat naar een container gaat, passeert DOCKER-USER vóór de eigen accept-regels van Docker, en Docker schrijft zelf nooit regels naar deze chain. De chain is bedoeld voor uw eigen regels en Docker laat de inhoud ervan ongemoeid bij het herstarten van de daemon.

Eén valkuil vóór het commando: tegen de tijd dat een pakket DOCKER-USER bereikt, heeft de DNAT-herschrijving al plaatsgevonden. De doelpoort van het pakket is de containerpoort (80 in ons voorbeeld), niet de gepubliceerde poort (8080). Een regel die matcht op --dport 8080 zal daarom niets vinden. De betrouwbare methode is matchen op de poort 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

Lees dit als volgt: voor pakketten die binnenkwamen op eth0 en behoren tot een verbinding waarvan de oorspronkelijke doelpoort 8080 was, drop alles wat niet afkomstig is van 10.0.0.10. De --ctdir ORIGINAL match beperkt de regel tot de richting van client naar container, zodat antwoordpakketten niet per ongeluk worden tegengehouden. Vervang eth0 door uw publieke interface; ip route | grep default benoemt deze. Test het op dezelfde manier als voorheen: curl vanaf het toegestane adres slaagt, en vanaf elke andere locatie loopt de verbinding vast (timeout). Die vertraging is het kenmerk van een DROP regel die zijn werk doet, in plaats van een poort waar niets achter zit. Het verschil tussen een geweigerde verbinding en een verbinding die in een timeout loopt is de snelste manier om een gefilterde poort te onderscheiden van een service die simpelweg niet luistert.

Regels die zijn toegevoegd met het iptables commando verdwijnen na een reboot. Aangezien UFW deze firewall al beheert, is de juiste plek om ze persistent 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 voert dit bestand opnieuw uit bij elke reload en elke boot, waardoor uw containerfiltering nu op dezelfde plek staat als de rest van uw firewall en zowel een reboot als een Docker-upgrade overleeft.

Waarom u de iptables-integratie van Docker niet moet uitschakelen

Oudere antwoorden op dit probleem suggereren het instellen van { "iptables": false } in /etc/docker/daemon.json. Doe dit niet. De firewallregels van Docker doen veel meer dan alleen poorten publiceren. De masquerade-regel zorgt ervoor dat containers uitgaande internettoegang hebben via het adres van de host. Als de integratie is uitgeschakeld, kunnen containers geen images ophalen, pakketmirrors bereiken of externe API's (application programming interfaces) aanroepen. De DNAT-regels zorgen ervoor dat -p überhaupt werkt; zonder deze regels stoppen gepubliceerde poorten volledig met functioneren. Ook de isolatieregels die afzonderlijke Compose-netwerken gescheiden houden, vervallen. U lost het omzeilingsprobleem op door de netwerkfunctionaliteit van containers te verbreken, en u bent vervolgens zelf verantwoordelijk voor het schrijven en onderhouden van al deze regels. De documentatie van Docker beschrijft deze instelling als een optie voor gebruikers die precies dat van plan zijn. De DOCKER-USER-chain bestaat juist zodat niemand deze schakelaar nodig heeft.

De IPv6-kant van hetzelfde probleem

Controleer eerst hoe de gepubliceerde poort eruitziet op IPv6:

sudo ss -tlnp | grep 8080

Sinds Docker Engine 27 beheert Docker standaard ip6tables. Op een Docker-netwerk met ingeschakelde IPv6 krijgt een gepubliceerde poort dezelfde DNAT-behandeling in de IPv6-tabellen. Hierdoor bestaat dezelfde omzeiling en is dezelfde oplossing van toepassing: de DOCKER-USER-chain bestaat ook in ip6tables. Spiegel daarom uw regel met sudo ip6tables -I DOCKER-USER ... en test vanaf een externe locatie 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, een standaard user-space proces dat luistert op [::]:8080 en het verkeer via IPv4 doorstuurt naar de container. Verkeer naar een host-proces verloopt wel via INPUT, waardoor UFW dat pad kan filteren, maar alleen als UFW IPv6 überhaupt beheert. Of dit het geval is, en op welke andere manieren er een IPv6-lek kan ontstaan op een VPS, is het onderwerp van de handleiding over UFW en IPv6.

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

Het patroon dat standhoudt

  • Publiceer elke interne poort op 127.0.0.1, zodat deze in de eerste plaats nooit wordt blootgesteld.
  • Wijs de publieke kant toe aan één reverse proxy die poort 80 en 443 beheert.
  • Hanteer de standaard 'deny'-instelling van UFW voor de host en sta alleen SSH en de proxy-poorten toe.
  • Filter daadwerkelijk publieke containerpoorten in DOCKER-USER, gekoppeld aan de oorspronkelijke doelpoort, en sla deze persistent op in /etc/ufw/after.rules.
  • Laat de iptables-integratie van Docker ingeschakeld.

Eenmaal ingesteld, voorkomt dit verrassingen: ufw status beschrijft de host en DOCKER-USER beschrijft de containers. Niets wordt per ongeluk gepubliceerd en het volgende docker run -p-commando dat u typt, stelt precies bloot wat u bedoelde.

FAQ

Waarom is mijn Docker-container bereikbaar terwijl UFW de poort blokkeert?

Omdat Docker de poort publiceert met een DNAT-regel in de PREROUTING-chain. Hierdoor wordt de bestemming van het pakket herschreven naar het adres van de container voordat er enige filtering plaatsvindt. Het pakket volgt vervolgens het FORWARD-pad, terwijl de regels van UFW zich in INPUT bevinden; een chain waar het pakket nooit doorheen komt. De firewall wordt niet geraadpleegd, waardoor de weigerregels geen effect hebben op gepubliceerde containerpoorten.

Hoe zorg ik ervoor dat UFW de gepubliceerde poorten van Docker blokkeert?

UFW kan dit zelf niet, omdat de regels in de verkeerde chain staan. Stop met het blootstellen van de poort door deze te publiceren als 127.0.0.1:8080:80, zodat alleen de host deze kan bereiken, of filter in de DOCKER-USER-chain met een iptables-regel die via conntrack overeenkomt met de oorspronkelijke doelpoort. Sla die regel permanent op in /etc/ufw/after.rules zodat deze na een herstart en ufw reload behouden blijft.

Moet ik "iptables": false instellen in de daemon.json van Docker?

Nee. Die instelling verwijdert alle firewall- en NAT-regels van Docker, wat veel meer problemen veroorzaakt dan alleen de bypass. Containers verliezen hun uitgaande internettoegang omdat de masquerade-regel verdwijnt, en gepubliceerde poorten werken niet meer omdat de DNAT-regels ontbreken. Gebruik publicatie via loopback en de DOCKER-USER-chain; hiermee lost u de blootstelling op zonder de netwerkfunctionaliteit van de container te verbreken.

Omzeilt Docker UFW ook op IPv6?

Vanaf Docker Engine 27 en later is het beheer van ip6tables standaard ingeschakeld. Een poort die op een IPv6-geschikt Docker-netwerk wordt gepubliceerd, wordt daarom net als bij IPv4 om UFW heen herschreven. Hiervoor is dezelfde DOCKER-USER-regel nodig, gespiegeld met ip6tables. Op netwerken zonder IPv6 luistert het docker-proxy-proces op [::] en dat verkeer passeert wel INPUT, waar UFW het kan filteren als UFW IPv6 beheert. Publiceren op 127.0.0.1 voorkomt beide situaties, omdat er dan helemaal niets op IPv6 luistert.