Docker umgeht UFW: Ursache und Lösungen
Docker schreibt DNAT Regeln direkt in die iptables, wodurch UFW Regeln ignoriert werden. Erfahren Sie, wie Sie die Container-Ports sicher absichern.
Warum Docker UFW umgeht
Docker umgeht UFW, da veröffentlichte Container-Ports die von UFW verwalteten Firewall-Regeln niemals passieren. Wenn Sie docker run -p 8080:80 ausführen, schreibt Docker eine DNAT (destination network address translation) Regel in die PREROUTING chain der nat table des Kernels. Diese Regel schreibt das Ziel jedes Pakets auf die private Adresse des Containers um, bevor der Kernel den Zielort des Pakets bestimmt. Das umgeschriebene Paket wird dann über die FORWARD chain in den Container weitergeleitet, die von Docker kontrolliert wird. Die Regeln von UFW befinden sich in der INPUT chain, und das Paket gelangt nie in diese. Daher zeigt ufw status ein Standard-Deny an, sudo ufw deny 8080 meldet Erfolg, und Port 8080 antwortet weiterhin auf das gesamte Internet.
Dies ist kein Docker-Bug und UFW ist nicht defekt. Beide Tools programmieren dieselbe Kernel-Firewall. Die Regeln von Docker greifen einfach an einem früheren Punkt auf dem Pfad des Pakets, sodass UFW nicht kontaktiert wird. Diese Anleitung demonstriert die Umgehung, erklärt den Mechanismus und behandelt anschließend die zwei funktionierenden Lösungen: das Veröffentlichen von Ports auf 127.0.0.1 und das Filtern in der DOCKER-USER chain. Falls Sie mit UFW neu sind, richten Sie es zuerst mit dem UFW-Firewall-Basishandbuch ein, da eine Default-Deny-Firewall weiterhin die richtige Basis für alle weiteren Dienste auf dem Server ist.
See the bypass on your own server
Start from a VPS where UFW is active with a default deny policy for incoming traffic. Run a web container with a published port:
sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpineufw status verbose shows Default: deny (incoming), allow (outgoing) and no rule for port 8080. By the firewall's own report, the port is closed. Now test from a different machine, not from the server itself:
curl -I http://your-vps-ip:8080/HTTP/1.1 200 OKThe container answers. Add an explicit deny rule and test again:
sudo ufw deny 8080/tcpThe port still answers, because the deny rule sits in a chain the packet never visits. UFW did not fail. It was never consulted. This is also why the problem hides so well: no error is printed anywhere, the deploy works, and the firewall status output looks exactly like a healthy locked-down server.
Der Mechanismus: PREROUTING läuft vor INPUT
Der Kernel verarbeitet ein eingehendes Paket in einer festen Reihenfolge. Das Problem ergibt sich aus dieser Reihenfolge.
PREROUTINGwird zuerst ausgeführt. Regeln hier können das Ziel des Pakets umschreiben. Die Docker-Regel für einen veröffentlichten Port macht genau das.- Die Routing-Entscheidung folgt als Nächstes. Ein Paket, das an den Host selbst gerichtet ist, geht in die
INPUT-Chain. Ein Paket, das an eine andere Maschine gerichtet ist, geht in dieFORWARD-Chain. - Die Regeln von UFW befinden sich in
INPUT. Die Regeln von Docker befinden sich inFORWARD.
Betrachten Sie die Docker-Regel für den gerade gestarteten Container:
sudo iptables -t nat -L DOCKER -nChain 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:80Die DNAT-Zeile ist entscheidend. Jedes Paket, das für Port 8080 eintrifft, erhält ein umgeschriebenes Ziel zu 172.17.0.2:80, der Adresse des Containers im privaten Bridge-Netzwerk von Docker. Nach der Umschreibung ist das Paket nicht mehr an den Host gerichtet. Die Routing-Entscheidung leitet es daher über den FORWARD-Pfad weiter, wo Docker bereits Regeln hinzugefügt hat, die den Datenverkehr in seine eigenen Netzwerke zulassen. Ihre deny 8080/tcp-Regel wartet in INPUT auf ein Paket, das niemals eintrifft.
Unter Ubuntu 24.04 ist der Befehl iptables ein Front-End für nftables, aber die Reihenfolge der Chains und das Ergebnis sind identisch. UFW und Docker schreiben beide in dieselbe Kernel-Packet-Pipeline, wobei der Einstiegspunkt von Docker früher liegt.
Die Standardlösung: Ports auf 127.0.0.1 veröffentlichen
Die meisten Container müssen nicht öffentlich erreichbar sein. Eine Datenbank, ein App-Server hinter einem Reverse Proxy, ein Admin-Panel oder ein Metrics-Endpoint: Keiner dieser Dienste sollte direkt über das Internet erreichbar sein. Veröffentlichen Sie diese auf der Loopback-Adresse:
docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpineOder in einer Compose-Datei:
services:
web:
image: nginx:1.29-alpine
ports:
- "127.0.0.1:8080:80"Dies funktioniert, weil die DNAT-Regel von Docker nun nur noch Pakete matcht, die an 127.0.0.1 adressiert sind. Ein Paket aus dem Internet kann diese Zieladresse niemals legitim enthalten, daher verwirft der Kernel es, bevor eine Firewall-Regel ausgeführt wird. Der Port ist nur vom Host aus erreichbar, von keinem anderen System. Überprüfen Sie die Bindung:
sudo ss -tlnp | grep 8080In der Ausgabe sollte 127.0.0.1:8080 stehen, nicht 0.0.0.0:8080 oder [::]:8080. Bestätigen Sie anschließend von einem anderen Rechner aus, dass der Zugriff auf curl http://your-vps-ip:8080/ verweigert wird.
Für Dienste, die öffentlich erreichbar sein sollen, nutzen Sie einen Reverse Proxy, der die Ports 80 und 443 belegt und die Weiterleitung per Hostname übernimmt. Veröffentlichen Sie keine weiteren Ports. Dies ist das Muster, auf dem der Traefik reverse proxy guide aufbaut. So bleibt auch eine selbst gehostete App wie Nextcloud on a VPS nur über den Proxy erreichbar. Wie ports:-Einträge deklariert werden und wie der restliche Compose-Workflow aussieht, wird im the Docker Compose basics guide erklärt.
Wenn alle internen Container auf Loopback laufen, übernimmt UFW wieder seine normale Aufgabe: den Schutz der Ports, die der Host selbst bereitstellt. Erstellen Sie den Regelsatz hier und führen Sie die Befehle in dieser Reihenfolge aus:
Echtes Filtern: die DOCKER-USER-Chain
Manchmal muss ein Container-Port im Netzwerk veröffentlicht bleiben, aber eingeschränkt werden. Ein Beispiel ist ein Datenbank-Replika-Port, der nur von einer bestimmten Büro-Adresse erreichbar sein darf. Hierfür stellt Docker die DOCKER-USER-Chain bereit. Jedes Paket, das an einen Container gerichtet ist, durchläuft DOCKER-USER, bevor die eigenen Accept-Rules von Docker greifen. Docker schreibt niemals Regeln in diese Chain. Die Chain ist für Ihre eigenen Regeln gedacht, und Docker lässt ihren Inhalt bei Neustarts des Daemons unverändert.
Ein Stolperstein vor dem Befehl: Sobald ein Paket DOCKER-USER erreicht, wurde die DNAT-Umschreibung bereits durchgeführt. Das Zielport des Pakets ist das Container-Port (80 in unserem Beispiel) und nicht das veröffentlichte Port (8080). Eine Regel, die auf --dport 8080 prüft, trifft daher auf nichts. Der zuverlässige Weg besteht darin, das Port zu prüfen, das der Client ursprünglich aufgerufen hat und das der Connection Tracker des Kernels speichert:
sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROPDies bedeutet: Für Pakete, die über eth0 eingegangen sind und zu einer Verbindung gehören, deren ursprüngliches Zielport 8080 war, werden alle Pakete verworfen, die nicht von 10.0.0.10 gesendet wurden. Der --ctdir ORIGINAL-Match beschränkt die Regel auf die Richtung Client-zu-Container, sodass Antwortpakete nicht fälschlicherweise blockiert werden. Ersetzen Sie eth0 durch Ihr öffentliches Interface; ip route | grep default benennt dieses. Testen Sie es wie zuvor: curl von der erlaubten Adresse ist erfolgreich, von überall sonst läuft die Verbindung in ein Timeout.
Mit dem iptables-Befehl hinzugefügte Regeln gehen beim Neustart verloren. Da UFW diese Firewall bereits verwaltet, ist der richtige Ort für dauerhafte Regeln /etc/ufw/after.rules. Fügen Sie am Ende der Datei einen Block hinzu:
*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
COMMITFühren Sie anschließend sudo ufw reload aus. UFW spielt diese Datei bei jedem Reload und jedem Bootvorgang erneut ab. Ihre Container-Filterung befindet sich nun am selben Ort wie der Rest Ihrer Firewall und bleibt sowohl nach einem Reboot als auch nach einem Docker-Upgrade erhalten.
Warum Sie die iptables-Integration von Docker nicht deaktivieren sollten
Ältere Antworten auf dieses Problem empfehlen, { "iptables": false } in /etc/docker/daemon.json zu setzen. Tun Sie dies nicht. Die Firewall-Regeln von Docker erledigen mehr als nur das Veröffentlichen von Ports. Die Masquerade-Regel ermöglicht Containern den ausgehenden Internetzugriff über die Adresse des Hosts. Wenn die Integration deaktiviert ist, können Container keine Images ziehen, keine Paket-Mirrors erreichen und keine externen APIs (Application Programming Interfaces) aufrufen. Die DNAT-Regeln sind die Voraussetzung für die Funktion von -p; ohne sie funktionieren veröffentlichte Ports nicht mehr. Auch die Isolationsregeln, die verschiedene Compose-Netzwerke trennen, fallen weg. Durch das Beheben des Umgehungspfads wird die Container-Netzwerkkommunikation zerstört. Jede dieser Regeln müsste dann manuell geschrieben und gepflegt werden. Die Dokumentation von Docker beschreibt diese Einstellung als eine Option für Benutzer, die genau das beabsichtigen. Die DOCKER-USER-Chain existiert gerade deshalb, damit niemand diesen Schalter benötigt.
Die IPv6-Seite desselben Problems
Prüfen Sie zuerst, wie der veröffentlichte Port unter IPv6 aussieht:
sudo ss -tlnp | grep 8080Seit Docker Engine 27 verwaltet Docker ip6tables standardmäßig. In einem Docker-Netzwerk mit aktiviertem IPv6 erhält ein veröffentlichter Port die gleiche DNAT-Behandlung in den IPv6-Tabellen. Daher existiert dort dieselbe Umgehung und dieselbe Lösung ist anwendbar: Die DOCKER-USER-Chain existiert ebenfalls in ip6tables. Spiegeln Sie Ihre Regel mit sudo ip6tables -I DOCKER-USER ... und testen Sie dies von extern mit curl gegen die öffentliche IPv6-Adresse Ihres Servers, zum Beispiel curl -6 http://[2001:db8:2a::1]:8080/.
In einem Netzwerk ohne IPv6 werden IPv6-Clients stattdessen durch docker-proxy verarbeitet, einen normalen User-Space-Prozess, der auf [::]:8080 lauscht und den Traffic über IPv4 in den Container weiterleitet. Traffic an einen Host-Prozess durchläuft INPUT, sodass UFW diesen Pfad filtern kann, allerdings nur, wenn UFW IPv6 überhaupt verwaltet. Ob dies der Fall ist und welche anderen Wege eine IPv6-Lücke auf einem VPS öffnen, ist Thema von dem UFW und IPv6 Guide.
Das Veröffentlichen auf Loopback umgeht die gesamte Fragestellung: -p 127.0.0.1:8080:80 bindet nur an IPv4-Loopback, es gibt also keinen IPv6-Listener und nichts, das von extern über einen der beiden Stacks erreichbar wäre.
Das zugrunde liegende Muster
- Veröffentlichen Sie jeden internen Port auf
127.0.0.1, damit dieser von vornherein nicht exponiert wird. - Weisen Sie der öffentlichen Seite einen Reverse Proxy zu, der die Ports 80 und 443 besitzt.
- Nutzen Sie UFW mit der Standardeinstellung
denyfür den Host und erlauben Sie nur SSH und die Proxy-Ports. - Filtern Sie tatsächlich öffentliche Container-Ports in
DOCKER-USER, basierend auf dem ursprünglichen Zielport, der in/etc/ufw/after.rulesgespeichert ist. - Lassen Sie die iptables-Integration von Docker aktiviert.
Einmal eingerichtet, verhindert dies unerwartete Konfigurationen: ufw status beschreibt den Host und DOCKER-USER beschreibt die Container. Nichts wird versehentlich veröffentlicht, und der nächste docker run -p, den Sie eingeben, exponiert exakt das, was beabsichtigt war.
FAQ
Warum kann ich meinen Docker-Container erreichen, obwohl UFW den Port blockiert?
Weil Docker den Port mit einer DNAT-Regel in der PREROUTING Chain veröffentlicht. Diese Regel schreibt das Ziel des Pakets auf die Adresse des Containers um, bevor eine Filterung stattfindet. Das Paket folgt dann dem FORWARD Pfad. Die UFW-Regeln befinden sich in der INPUT Chain, welche das Paket nie erreicht. Die Firewall wird nicht abgefragt, weshalb ihre Deny-Regeln bei veröffentlichten Container-Ports keine Wirkung haben.
Wie kann ich UFW dazu bringen, die veröffentlichten Docker-Ports zu blockieren?
UFW selbst kann dies nicht, da seine Regeln in der falschen Chain liegen. Entweder Sie hören auf, den Port zu exponieren, indem Sie ihn als 127.0.0.1:8080:80 veröffentlichen, sodass nur der Host ihn erreichen kann, oder Sie filtern in der DOCKER-USER Chain mit einer iptables-Regel, die über conntrack das ursprüngliche Zielport erkennt. Speichern Sie diese Regel in /etc/ufw/after.rules, damit sie Neustarts und ufw reload übersteht.
Sollte ich "iptables": false in der Docker daemon.json setzen?
Nein. Diese Einstellung entfernt alle Firewall- und NAT-Regeln von Docker. Das verursacht weitaus mehr Probleme als die Umgehung der Firewall. Container verlieren den ausgehenden Internetzugriff, da die Masquerade-Regel fehlt. Veröffentlichte Ports funktionieren nicht mehr, da die DNAT-Regeln fehlen. Nutzen Sie stattdessen Loopback-Publishing und die DOCKER-USER Chain; diese lösen das Expositions-Problem, ohne das Container-Networking zu unterbrechen.
Umgeht Docker UFW auch bei IPv6?
Bei Docker Engine 27 und neuer ist das ip6tables-Management standardmäßig aktiviert. Ein Port, der in einem IPv6-fähigen Docker-Netzwerk veröffentlicht wurde, wird genau wie bei IPv4 an UFW vorbeigeschrieben. Er benötigt dieselbe DOCKER-USER Regel, die mit ip6tables gespiegelt werden muss. In Netzwerken ohne IPv6 lauscht der docker-proxy Prozess auf [::]. Dieser Datenverkehr durchläuft INPUT, wo UFW ihn filtern kann, sofern UFW IPv6 verwaltet. Das Veröffentlichen auf 127.0.0.1 vermeidet beide Fälle, da dort gar nichts auf IPv6 lauscht.