SSD Nodes Learn Hosting plans →
Anleitungen Matt ConnorVon Matt Connor · Aktualisiert 2026-08-25

Docker umgeht UFW: Ports richtig absichern

Docker leitet veröffentlichte Ports an UFW vorbei. So erklären Sie den Bypass und sperren Ports korrekt, damit ein deny nicht weiter Verbindungen aus dem Internet zulässt.

Warum Docker UFW umgeht

Docker umgeht UFW, weil veröffentlichte Container-Ports nie die Firewall-Regeln durchlaufen, die UFW verwaltet. Wenn Sie docker run -p 8080:80 ausführen, schreibt Docker eine DNAT-Regel (Destination Network Address Translation) in die PREROUTING-Chain der nat-Tabelle des Kernels. Diese Regel ändert das Ziel jedes Pakets in die private Adresse des Containers, bevor der Kernel entscheidet, wohin das Paket weitergeleitet wird. Das geänderte Paket wird anschließend durch die FORWARD-Chain, die Docker kontrolliert, an den Container weitergeleitet. Die Regeln von UFW befinden sich in der INPUT-Chain, und das Paket erreicht diese Chain nie. Deshalb zeigt ufw status standardmäßig deny an, sudo ufw deny 8080 meldet Erfolg, und Port 8080 nimmt weiterhin Verbindungen aus dem gesamten Internet an.

Das ist kein Docker-Fehler, und UFW ist nicht defekt. Beide Werkzeuge konfigurieren dieselbe Kernel-Firewall. Die Regeln von Docker greifen lediglich früher im Paketpfad, sodass UFW gar nicht abgefragt wird. Dieser Leitfaden demonstriert den Bypass, erklärt den Mechanismus und behandelt anschließend die beiden funktionierenden Lösungen: Ports auf 127.0.0.1 veröffentlichen und in der DOCKER-USER-Chain filtern. Wenn UFW für Sie neu ist, richten Sie es zunächst anhand von der Anleitung zu den UFW-Firewall-Grundlagen ein, denn eine Firewall mit standardmäßigem deny bleibt die richtige Grundlage für alle anderen Dienste auf dem Server.

Den Bypass auf Ihrem eigenen Server nachvollziehen

Verwenden Sie zunächst einen VPS, auf dem UFW mit einer standardmäßigen Deny-Richtlinie für eingehenden Netzwerkverkehr aktiv ist. Starten Sie einen Webcontainer mit einem veröffentlichten Port:

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

ufw status verbose zeigt Default: deny (incoming), allow (outgoing) und keine Regel für Port 8080. Laut dem eigenen Status von UFW ist der Port geschlossen. Testen Sie nun von einem anderen Rechner aus, nicht vom Server selbst:

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

Der Container antwortet. Fügen Sie eine explizite Deny-Regel hinzu und testen Sie erneut:

sudo ufw deny 8080/tcp

Der Port antwortet weiterhin, weil sich die Deny-Regel in einer Chain befindet, die das Paket nie durchläuft. UFW ist nicht fehlgeschlagen. Die Firewall wurde nie abgefragt. Deshalb bleibt das Problem auch so gut verborgen: An keiner Stelle wird ein Fehler ausgegeben, das Deployment funktioniert, und die Statusausgabe der Firewall sieht exakt wie die eines korrekt abgesicherten Servers aus.

Der Mechanismus: PREROUTING läuft vor INPUT

Der Kernel verarbeitet ein eingehendes Paket in einer festen Reihenfolge. Das gesamte Problem ergibt sich aus dieser Reihenfolge.

  1. PREROUTING wird zuerst verarbeitet. Regeln in dieser Chain können das Ziel des Pakets ändern. Genau das tut auch Dockers Regel für einen veröffentlichten Port.
  2. Danach folgt die Routing-Entscheidung. Ein an den Host selbst adressiertes Paket wird an die INPUT-Chain übergeben. Ein an einen anderen Rechner adressiertes Paket wird an die FORWARD-Chain übergeben.
  3. Die Regeln von UFW befinden sich in INPUT. Die Regeln von Docker befinden sich in FORWARD.

Sehen Sie sich die Docker-Regel für den gerade gestarteten Container an:

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

Die Zeile DNAT erklärt das gesamte Verhalten. Für jedes Paket, das an Port 8080 eintrifft, wird das Ziel in 172.17.0.2:80 geändert, die Adresse des Containers im privaten Docker-Bridge-Netzwerk. Nach dieser Änderung ist das Paket nicht mehr an den Host adressiert. Daher führt die Routing-Entscheidung das Paket über den FORWARD-Pfad weiter. Dort hat Docker bereits Regeln hinzugefügt, die den Netzwerkverkehr in die eigenen Netzwerke akzeptieren. Ihre deny 8080/tcp-Regel wartet in INPUT auf ein Paket, das nie eintrifft.

Unter Ubuntu 24.04 ist der Befehl iptables ein Frontend für nftables. Die Reihenfolge der Chains und das Ergebnis sind jedoch identisch. UFW und Docker schreiben beide in dieselbe Paketverarbeitung des Kernels, und Docker greift früher ein. Dieses Verhalten ist nicht spezifisch für UFW: firewalld auf einem Rocky- oder AlmaLinux-VPS filtert an derselben Stelle dieser Verarbeitung und wird durch dieselbe DNAT-Regel umgangen. Daher gelten die folgenden Korrekturen auch dort.

Die alltägliche Lösung: Ports auf 127.0.0.1 veröffentlichen

Die meisten Container mussten von Anfang an nicht öffentlich erreichbar sein. Eine Datenbank, ein App-Server hinter einem Reverse Proxy, ein Administrationsbereich oder ein Metrik-Endpunkt sollte nicht direkt auf Anfragen aus dem Internet antworten. Veröffentlichen Sie diese Dienste auf der Loopback-Adresse:

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

Oder in einer Compose-Datei:

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

Das funktioniert, weil die DNAT-Regel von Docker jetzt nur Pakete erfasst, die an 127.0.0.1 adressiert sind. Ein Paket aus dem Internet kann dieses Ziel niemals legitim tragen. Der Kernel verwirft es daher, bevor eine Firewall-Regel ausgeführt wird. Der Port ist vom Host aus erreichbar, aber von keinem anderen System. Prüfen Sie die Bindung:

sudo ss -tlnp | grep 8080

In der Ausgabe muss 127.0.0.1:8080 erscheinen, nicht 0.0.0.0:8080 oder [::]:8080. Prüfen Sie anschließend von einem anderen Rechner, dass curl http://your-vps-ip:8080/ abgewiesen wird.

Für Dienste, die aus dem Internet erreichbar sein sollen, betreiben Sie einen einzigen Reverse Proxy, der die Ports 80 und 443 übernimmt und anhand des Hostnamens weiterleitet. Veröffentlichen Sie keine weiteren Ports. Dieses Muster wird im Leitfaden zum Traefik-Reverse-Proxy umgesetzt. So bleibt eine selbst gehostete Anwendung wie Nextcloud auf einem VPS ausschließlich über ihren Proxy erreichbar. Wie ports:-Einträge deklariert werden und wie der restliche Compose-Workflow funktioniert, wird im Grundlagenleitfaden zu Docker Compose beschrieben.

Wenn sich jeder interne Container auf der Loopback-Adresse befindet, erfüllt UFW wieder seine normale Aufgabe: Es schützt die Ports, die der Host selbst bereitstellt. Erstellen Sie den Regelsatz hier und führen Sie anschließend die Befehle in der angegebenen Reihenfolge aus:

ToolUFW rule generator

Die eigentliche Filterung: die DOCKER-USER-Chain

Manchmal muss ein Container-Port im Netzwerk veröffentlicht bleiben, aber eingeschränkt werden. Ein Beispiel ist der Port einer Datenbankreplikation, auf den nur eine Adresse im Büro zugreifen darf. Dafür stellt Docker die DOCKER-USER-Chain bereit. Jedes Paket zu einem beliebigen Container durchläuft DOCKER-USER, bevor Dockers eigene Accept-Regeln greifen. Docker schreibt niemals Regeln in diese Chain. Die Chain ist für Ihre Regeln vorgesehen, und Docker lässt ihren Inhalt auch nach Neustarts des Daemons unverändert.

Vor dem Befehl gibt es eine wichtige Besonderheit: Wenn ein Paket DOCKER-USER erreicht, wurde die DNAT-Umschreibung bereits durchgeführt. Der Zielport des Pakets ist dann der Container-Port (in unserem Beispiel 80) und nicht der veröffentlichte Port (8080). Eine Regel mit --dport 8080 würde daher kein Paket treffen. Zuverlässig ist es, den Port abzugleichen, den der Client ursprünglich angesprochen hat. Der Connection Tracker des Kernels speichert diesen Port:

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

Die Regel bedeutet: Für Pakete, die über eth0 eingegangen sind und zu einer Verbindung mit dem ursprünglichen Zielport 8080 gehören, werden alle Pakete verworfen, die nicht von 10.0.0.10 gesendet wurden. Der Match --ctdir ORIGINAL beschränkt die Regel auf die Richtung vom Client zum Container. Antwortpakete werden dadurch nicht versehentlich erfasst. Ersetzen Sie eth0 durch Ihr öffentliches Interface; ip route | grep default gibt dessen Namen aus. Testen Sie die Regel wie zuvor: curl von der erlaubten Adresse ist erfolgreich, von allen anderen Adressen läuft die Verbindung in einen Timeout. Dieses Hängenbleiben zeigt, dass eine DROP-Regel wie vorgesehen arbeitet und nicht, dass hinter dem Port kein Dienst vorhanden ist. Der Unterschied zwischen einer abgelehnten Verbindung und einer Verbindung mit Timeout ist der schnellste Weg, einen gefilterten Port von einem Dienst zu unterscheiden, der nicht lauscht.

Regeln, die mit dem Befehl iptables hinzugefügt wurden, verschwinden beim Reboot. Da UFW diese Firewall bereits verwaltet, ist /etc/ufw/after.rules der geeignete Ort, um sie dauerhaft zu speichern. Hängen Sie am Ende der Datei einen Block an:

*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

Führen Sie anschließend sudo ufw reload aus. UFW liest diese Datei bei jedem Reload und bei jedem Boot erneut ein. Dadurch befindet sich die Filterung Ihrer Container 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 das nicht. Die Firewall-Regeln von Docker veröffentlichen nicht nur Ports. Die Masquerade-Regel ermöglicht Containern den ausgehenden Internetzugriff über die Adresse des Hosts. Bei deaktivierter Integration können Container daher keine Images abrufen, keine Paketspiegel erreichen und keine externe API (application programming interface) aufrufen. Die DNAT-Regeln sorgen dafür, dass -p überhaupt funktioniert. Veröffentlichte Ports funktionieren dann vollständig nicht mehr. Auch die Isolationsregeln, die getrennte Compose-Netzwerke voneinander abschirmen, entfallen. Sie würden die Umgehung beheben, indem Sie die Container-Netzwerkfunktion zerstören. Jede dieser Regeln müssten Sie anschließend selbst schreiben und manuell verwalten. In der Dokumentation von Docker wird diese Einstellung ausdrücklich für Benutzer beschrieben, die genau das beabsichtigen. Die Kette DOCKER-USER existiert genau deshalb, damit niemand diesen Schalter benötigt.

Die IPv6-Seite desselben Problems

Prüfen Sie zunächst, wie der veröffentlichte Port unter IPv6 aussieht:

sudo ss -tlnp | grep 8080

Seit Docker Engine 27 verwaltet Docker standardmäßig ip6tables. In einem Docker-Netzwerk mit aktiviertem IPv6 wird ein veröffentlichter Port in den IPv6-Tabellen ebenfalls per DNAT umgesetzt. Daher besteht dort dieselbe Umgehungsmöglichkeit, und dieselbe Korrektur gilt: Die Kette DOCKER-USER ist auch in ip6tables vorhanden. Spiegeln Sie Ihre Regel daher mit sudo ip6tables -I DOCKER-USER ... und testen Sie sie von außerhalb 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 von docker-proxy verarbeitet, einem normalen User-Space-Prozess, der auf [::]:8080 lauscht und den Datenverkehr über IPv4 in den Container weiterleitet. Datenverkehr zu einem Host-Prozess durchläuft INPUT. UFW kann diesen Pfad daher filtern, allerdings nur, wenn UFW IPv6 überhaupt verwaltet. Ob dies der Fall ist und auf welche andere Weise auf einem VPS eine IPv6-Lücke entsteht, wird im UFW- und IPv6-Leitfaden erläutert.

Die Veröffentlichung auf dem Loopback-Interface umgeht die gesamte Frage: -p 127.0.0.1:8080:80 bindet ausschließlich an das IPv4-Loopback-Interface. Daher gibt es keinen IPv6-Listener und von außerhalb über keinen der beiden Protokollstacks etwas zu erreichen.

Das Muster, das zuverlässig funktioniert

  • Veröffentlichen Sie jeden internen Port auf 127.0.0.1, damit er gar nicht erst nach außen erreichbar ist.
  • Überlassen Sie die öffentliche Seite einem einzigen Reverse Proxy, der die Ports 80 und 443 verwaltet.
  • Verwenden Sie für den Host weiterhin die UFW-Standardregel deny und erlauben Sie SSH sowie die Proxy-Ports.
  • Filtern Sie tatsächlich öffentliche Container-Ports in DOCKER-USER, basierend auf dem ursprünglichen Zielport, und speichern Sie die Regeln dauerhaft in /etc/ufw/after.rules.
  • Lassen Sie Dockers iptables-Integration aktiviert.

Nach der einmaligen Einrichtung gibt es keine Überraschungen mehr: ufw status beschreibt den Host und DOCKER-USER die Container. Nichts wird versehentlich veröffentlicht, und der nächste von Ihnen eingegebene docker run -p-Befehl veröffentlicht genau das, was Sie vorgesehen haben.

FAQ

Warum kann ich meinen Docker-Container erreichen, obwohl UFW den Port blockiert?

Docker veröffentlicht den Port mit einer DNAT-Regel in der Kette PREROUTING. Diese Regel schreibt das Ziel des Pakets auf die Adresse des Containers um, bevor eine Filterung stattfindet. Das Paket durchläuft anschließend den Pfad FORWARD. Die Regeln von UFW befinden sich in INPUT, einer Kette, die das Paket nicht erreicht. Die Firewall wird daher nie abgefragt. Ihre deny-Regeln haben auf veröffentlichte Container-Ports keine Wirkung.

Wie kann ich UFW die von Docker veröffentlichten Ports blockieren lassen?

Das ist mit UFW selbst nicht möglich, weil sich seine Regeln in der falschen Kette befinden. Beenden Sie entweder die Veröffentlichung des Ports, indem Sie ihn als 127.0.0.1:8080:80 veröffentlichen, sodass nur der Host darauf zugreifen kann. Alternativ filtern Sie in der Kette DOCKER-USER mit einer iptables-Regel, die den ursprünglichen Zielport über conntrack abgleicht. Speichern Sie diese Regel in /etc/ufw/after.rules, damit sie Reboots und ufw reload übersteht.

Sollte ich "iptables": false in Docker's daemon.json setzen?

Nein. Diese Einstellung entfernt alle Firewall- und NAT-Regeln von Docker. Dadurch wird mehr als nur die Umgehung von UFW beeinträchtigt. Container verlieren den ausgehenden Internetzugriff, weil die Masquerade-Regel entfernt wird. Veröffentlichte Ports funktionieren nicht mehr, weil die DNAT-Regeln entfernt werden. Verwenden Sie stattdessen die Veröffentlichung über Loopback und die Kette DOCKER-USER. Damit wird die Erreichbarkeit eingeschränkt, ohne die Netzwerkanbindung der Container zu beeinträchtigen.

Umgeht Docker UFW auch bei IPv6?

Bei Docker Engine 27 und höher ist die Verwaltung von ip6tables standardmäßig aktiviert. Daher wird ein Port, der in einem für IPv6 aktivierten Docker-Netzwerk veröffentlicht ist, genau wie bei IPv4 an UFW vorbei umgeschrieben. Dafür ist dieselbe DOCKER-USER-Regel erforderlich, ergänzt um ip6tables. In Netzwerken ohne IPv6 lauscht der Prozess docker-proxy auf [::]. Dieser Datenverkehr durchläuft INPUT. UFW kann ihn dort filtern, wenn UFW IPv6 verwaltet. Die Veröffentlichung auf 127.0.0.1 vermeidet beide Fälle, weil überhaupt nichts auf IPv6 lauscht.