Docker und die Ubuntu-Firewall UFW werden häufig gemeinsam auf Servern eingesetzt. Dabei gibt es jedoch eine wichtige Besonderheit: Ein mit Docker veröffentlichter Port kann trotz aktivierter UFW-Firewall von außen erreichbar sein.
Das Projekt ufw-docker schließt diese Lücke, ohne Dockers eigene Firewall- und Netzwerkverwaltung zu deaktivieren.
Projektseite:
https://github.com/chaifeng/ufw-docker
Das Problem: Docker umgeht UFW-Regeln
Normalerweise lässt sich ein Server mit UFW nach dem Prinzip „standardmäßig alles blockieren und nur benötigte Ports freigeben“ absichern:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw enable
Der Status sieht anschließend beispielsweise so aus:
Status: active
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
Starten wir nun einen Docker-Container und veröffentlichen dessen Port:
docker run -d \
--name nginx-test \
-p 8080:80 \
nginx:alpine
würde man erwarten, dass Port 8080 durch UFW blockiert wird. In vielen Docker-Konfigurationen ist der Port jedoch trotzdem von außen erreichbar.
Auch eine explizite Sperre löst das Problem nicht zuverlässig:
sudo ufw deny 8080/tcp
Der Grund dafür liegt in der Paketverarbeitung des Linux-Kernels. Docker erstellt eigene iptables- beziehungsweise nftables-Regeln und verarbeitet weitergeleiteten Datenverkehr teilweise, bevor die normalen UFW-Regeln für den Host greifen.
Das kann dazu führen, dass veröffentlichte Container-Ports unbeabsichtigt im Internet erreichbar sind. Genau dieses Verhalten beschreibt auch die Dokumentation von ufw-docker.
Was macht ufw-docker?
ufw-docker verbindet die Docker-Firewallkette DOCKER-USER mit den Forwarding-Regeln von UFW.
Dadurch können Zugriffe auf veröffentlichte Container-Ports über normale UFW-Routing-Regeln gesteuert werden.
Die Lösung verfolgt dabei mehrere wichtige Ziele:
- Dockers iptables-Funktion bleibt aktiviert.
- Container können weiterhin untereinander kommunizieren.
- Container behalten ihren Zugriff auf das Internet.
- Veröffentlichte Ports werden nicht automatisch aus dem Internet erreichbar.
- Einzelne Container und Ports können gezielt freigegeben werden.
- IPv4 und IPv6 werden unterstützt.
- Docker-Netzwerke müssen nicht vollständig manuell verwaltet werden.
ufw-docker ergänzt dafür unter anderem Regeln in:
/etc/ufw/after.rules
/etc/ufw/after6.rules
Außerdem verwendet das Projekt die von Docker vorgesehene Firewallkette:
DOCKER-USER
Voraussetzungen
Für die Installation benötigen wir:
- Ubuntu oder ein anderes System mit UFW
- eine funktionierende Docker-Installation
- Git
- Root- beziehungsweise sudo-Rechte
- aktiviertes IP-Forwarding
Zunächst aktualisieren wir die Paketquellen und installieren UFW sowie Git:
sudo apt update
sudo apt install -y ufw git
Anschließend prüfen wir Docker:
docker --version
Und UFW:
sudo ufw status verbose
Wichtige Vorbereitung
Docker sollte seine Firewallregeln weiterhin selbst verwalten dürfen.
In /etc/docker/daemon.json sollte daher nicht folgende Option stehen:
{
"iptables": false
}
Falls diese Einstellung vorhanden ist, sollte sie entfernt werden.
Eine mögliche Standardkonfiguration könnte beispielsweise so aussehen:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Anschließend wird Docker neu gestartet:
sudo systemctl restart docker
Auch die UFW-Forwarding-Policy sollte nicht aufgrund älterer Anleitungen pauschal auf ACCEPT gesetzt worden sein. ufw-docker ist darauf ausgelegt, mit Dockers normaler Firewallverwaltung und einer restriktiven UFW-Konfiguration zusammenzuarbeiten.
SSH-Zugriff freigeben
Bevor UFW aktiviert oder neu konfiguriert wird, muss der SSH-Port freigegeben werden. Andernfalls besteht die Gefahr, sich vom Server auszusperren.
Bei Verwendung des Standardports:
sudo ufw allow 22/tcp
Alternativ kann das vordefinierte OpenSSH-Profil verwendet werden:
sudo ufw allow OpenSSH
Danach setzen wir die grundlegenden Richtlinien:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw default deny routed
UFW kann nun aktiviert werden:
sudo ufw enable
ufw-docker herunterladen
Das Repository wird nach /opt geklont:
cd /opt
sudo git clone https://github.com/chaifeng/ufw-docker.git
cd ufw-docker
Das Hauptskript muss ausführbar sein:
sudo chmod +x ufw-docker
ufw-docker systemweit installieren
Für eine dauerhafte Installation verwenden wir:
sudo ./ufw-docker install --system
Die Option --system installiert das Programm unter:
/usr/local/bin/ufw-docker
Zusätzlich werden eine Manpage und ein systemd-Dienst eingerichtet. Der Dienst führt nach dem Start von Docker ein erneutes Laden der Regeln aus.
Nach der Installation starten wir UFW neu:
sudo systemctl restart ufw
Anschließend empfiehlt sich ein Neustart des Servers:
sudo reboot
Nach dem Neustart prüfen wir beide Dienste:
sudo systemctl status docker
sudo systemctl status ufw
Falls der ufw-docker-Dienst installiert wurde:
sudo systemctl status ufw-docker
Installation überprüfen
ufw-docker bringt eine eigene Prüffunktion mit:
sudo ufw-docker check
Die aktuell verwalteten Freigaben können mit folgendem Befehl angezeigt werden:
sudo ufw-docker list
Alternativ:
sudo ufw-docker status
Auch UFW selbst zeigt die erzeugten Regeln an:
sudo ufw status numbered
Docker-Netzwerke automatisch erkennen
Standardmäßig berücksichtigt ufw-docker die privaten IPv4-Netze:
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
Für IPv6 wird standardmäßig der ULA-Bereich verwendet:
fd00::/8
Bei individuellen Docker-Netzwerken kann ufw-docker die vorhandenen Subnetze automatisch ermitteln:
sudo ufw-docker install --docker-subnets --system
Die entsprechende Prüfung erfolgt mit:
sudo ufw-docker check --docker-subnets
Alternativ können Netzwerke explizit angegeben werden:
sudo ufw-docker install \
--docker-subnets \
172.20.0.0/16 \
172.21.0.0/16 \
192.168.100.0/24 \
fd00:20::/64 \
--system
Das Skript unterstützt sowohl automatisch erkannte als auch manuell angegebene Docker- und interne Netzwerke.
Erster Funktionstest
Wir starten erneut einen Nginx-Container:
docker run -d \
--name nginx-test \
-p 8080:80 \
nginx:alpine
Lokal sollte der Dienst erreichbar sein:
curl http://127.0.0.1:8080
Von einem externen Rechner sollte der Zugriff zunächst blockiert werden:
http://SERVER-IP:8080
Damit ist sichergestellt, dass ein veröffentlichter Docker-Port nicht automatisch öffentlich erreichbar wird.
Einen Container-Port freigeben
Um den Nginx-Container gezielt freizugeben, verwenden wir:
sudo ufw-docker allow nginx-test 80/tcp
Wichtig ist dabei:
Bei ufw-docker wird normalerweise der Port innerhalb des Containers angegeben, nicht der veröffentlichte Host-Port.
Bei dieser Zuordnung:
ports:
- "8080:80"
ist:
8080der Host-Port80der Container-Port
Der korrekte Befehl lautet daher:
sudo ufw-docker allow nginx-test 80/tcp
und nicht:
sudo ufw-docker allow nginx-test 8080/tcp
Das Skript ermittelt über docker inspect die IP-Adressen, Netzwerke und veröffentlichten Ports des Containers und erzeugt daraus passende UFW-Route-Regeln.
Alle veröffentlichten Ports eines Containers freigeben
Werden Port und Protokoll weggelassen, kann ufw-docker alle veröffentlichten Ports des Containers berücksichtigen:
sudo ufw-docker allow nginx-test
Für öffentlich erreichbare Server ist eine einzelne, explizite Freigabe jedoch meist sicherer:
sudo ufw-docker allow nginx-test 80/tcp
Eine Freigabe wieder entfernen
Die Regel für einen bestimmten Port wird folgendermaßen gelöscht:
sudo ufw-docker delete allow nginx-test 80/tcp
Alle Freigaben eines Containers lassen sich entfernen mit:
sudo ufw-docker delete allow nginx-test
Anschließend kontrollieren wir den Status:
sudo ufw-docker list
sudo ufw status numbered
Zugriff nur aus dem Heimnetz erlauben
Nicht jeder Container muss öffentlich erreichbar sein. Administrationsoberflächen wie Portainer, Grafana, Prometheus oder ein Traefik-Dashboard sollten häufig nur über das Heimnetz oder ein VPN erreichbar sein.
Eine solche Einschränkung kann direkt mit UFW als Routing-Regel umgesetzt werden.
Angenommen, der Container besitzt die IP-Adresse:
172.20.0.10
und soll auf Port 3000 nur aus dem Netz 192.168.1.0/24 erreichbar sein:
sudo ufw route allow proto tcp \
from 192.168.1.0/24 \
to 172.20.0.10 \
port 3000
Für ein WireGuard-Netz könnte die Regel beispielsweise so aussehen:
sudo ufw route allow proto tcp \
from 10.10.10.0/24 \
to 172.20.0.10 \
port 3000
Dabei ist zu beachten, dass sich die IP-Adresse eines Containers nach einer Neuerstellung ändern kann. Für dauerhaft IP-basierte Regeln empfiehlt sich daher eine feste Container-IP oder eine erneute Ausführung von ufw-docker reload.
Beispiel mit Docker Compose
Eine einfache compose.yml könnte so aussehen:
services:
nginx:
image: nginx:alpine
container_name: webserver
restart: unless-stopped
ports:
- "8080:80"
networks:
web:
ipv4_address: 172.30.0.10
networks:
web:
name: web
driver: bridge
ipam:
config:
- subnet: 172.30.0.0/24
Der Stack wird gestartet mit:
docker compose up -d
Danach geben wir den Container-Port 80 frei:
sudo ufw-docker allow webserver 80/tcp
Die Regel kann überprüft werden mit:
sudo ufw-docker list webserver
Beispiel für Traefik
Bei einem Reverse-Proxy-Setup sollten im Normalfall nur Traefik beziehungsweise der verwendete Proxy öffentlich erreichbar sein.
Eine typische Portkonfiguration sieht so aus:
services:
traefik:
image: traefik:v3
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
networks:
- proxy
networks:
proxy:
name: proxy
Die Freigaben erfolgen mit:
sudo ufw-docker allow traefik 80/tcp
sudo ufw-docker allow traefik 443/tcp
Andere Container benötigen keine veröffentlichten Ports, sofern Traefik sie über das gemeinsame Docker-Netzwerk erreicht:
services:
wordpress:
image: wordpress:latest
networks:
- proxy
- backend
networks:
proxy:
external: true
backend:
internal: true
Dadurch ergibt sich ein übersichtliches Sicherheitsmodell:
Internet
|
v
UFW / ufw-docker
|
v
Traefik: 80 und 443
|
v
interne Docker-Netzwerke
|
+-- WordPress
+-- Grafana
+-- Authelia
+-- weitere Dienste
Nur der Reverse Proxy ist direkt nach außen freigegeben. Die eigentlichen Anwendungen bleiben innerhalb der Docker-Netzwerke.
UDP-Port freigeben
Für einen DNS-Container wie Pi-hole oder dnscrypt-proxy wird neben TCP häufig UDP benötigt.
Beispiel:
sudo ufw-docker allow pihole 53/udp
sudo ufw-docker allow pihole 53/tcp
Auch hier wird der Container-Port verwendet.
Die zugrunde liegende UFW-Regel entspricht prinzipiell:
sudo ufw route allow proto udp from any to CONTAINER-IP port 53
Regeln nach einer Container-Neuerstellung aktualisieren
Wird ein Container neu erstellt, kann er eine andere interne IP-Adresse erhalten. Die vorhandene Firewallregel verweist dann möglicherweise noch auf die alte Adresse.
ufw-docker kann die Regeln neu laden:
sudo ufw-docker reload
Dabei liest das Skript die vorhandenen, von ufw-docker markierten Regeln aus und gleicht sie erneut mit den aktuell laufenden Containern ab. Nicht mehr vorhandene Regeln werden entfernt und geänderte Container-IP-Adressen aktualisiert.
Bei systemweiter Installation übernimmt dies zusätzlich der systemd-Dienst nach dem Start von Docker.
Logs kontrollieren
Blockierte Docker-Verbindungen werden mit folgendem Präfix protokolliert:
[UFW DOCKER BLOCK]
Die Meldungen können über das Journal gesucht werden:
sudo journalctl -k | grep "UFW DOCKER BLOCK"
Alternativ:
sudo dmesg | grep "UFW DOCKER BLOCK"
Je nach UFW-Konfiguration befinden sich die Meldungen auch in:
/var/log/ufw.log
Die letzten Einträge lassen sich anzeigen mit:
sudo tail -f /var/log/ufw.log
Falls das Logging noch nicht aktiviert ist:
sudo ufw logging medium
Firewallregeln überprüfen
Die wichtigsten Prüfkommandos sind:
sudo ufw status verbose
sudo ufw status numbered
sudo ufw-docker status
sudo ufw-docker check
Die DOCKER-USER-Kette lässt sich mit iptables anzeigen:
sudo iptables -L DOCKER-USER -n -v --line-numbers
Auf nftables-basierten Systemen kann zusätzlich geprüft werden:
sudo nft list ruleset
Docker selbst zeigt seine veröffentlichten Ports mit:
docker ps --format "table {{.Names}}\t{{.Ports}}"
Eine detaillierte Prüfung eines Containers erfolgt mit:
docker inspect webserver
Sicherheitsprüfung mit nmap
Die Firewall sollte nicht nur vom Server selbst, sondern auch von einem externen System getestet werden.
Beispiel:
nmap -Pn -p 22,80,443,8080 SERVER-IP
Vor der Freigabe könnte das Ergebnis so aussehen:
22/tcp open
80/tcp filtered
443/tcp filtered
8080/tcp filtered
Nach der Freigabe von Traefik:
sudo ufw-docker allow traefik 80/tcp
sudo ufw-docker allow traefik 443/tcp
sollten nur die gewünschten Ports erreichbar sein:
22/tcp open
80/tcp open
443/tcp open
8080/tcp filtered
Die Prüfung sollte nach Möglichkeit über einen echten externen Internetanschluss erfolgen, beispielsweise über ein Mobilfunknetz.
IPv6 nicht vergessen
Ein häufiger Fehler besteht darin, nur IPv4 zu testen.
Besitzt der Server eine öffentliche IPv6-Adresse, müssen auch die IPv6-Regeln geprüft werden:
sudo ufw status verbose
In /etc/default/ufw sollte IPv6 aktiviert sein:
IPV6=yes
Danach:
sudo systemctl restart ufw
ufw-docker bearbeitet neben /etc/ufw/after.rules auch:
/etc/ufw/after6.rules
Das aktuelle Skript erzeugt dabei eigene IPv6-Forwarding- und Logging-Regeln und verwendet ebenfalls die DOCKER-USER-Kette.
Ein externer IPv6-Portscan kann beispielsweise so ausgeführt werden:
nmap -6 -Pn -p 22,80,443 SERVER-IPV6
Manuelle Alternative ohne Hilfsskript
ufw-docker kann auch ohne dauerhafte Installation des Skripts verwendet werden. Das Projekt ergänzt dazu einen Regelblock in /etc/ufw/after.rules, der unter anderem folgende Ketten verbindet:
DOCKER-USER
ufw-user-forward
ufw-docker-logging-deny
Ein wesentlicher Bestandteil ist:
-A DOCKER-USER -j ufw-user-forward
Dadurch werden UFW-Routing-Regeln für den Docker-Datenverkehr berücksichtigt.
Bestehende sowie zugehörige Verbindungen werden erlaubt:
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
Ungültige Verbindungen werden verworfen:
-A DOCKER-USER -m conntrack --ctstate INVALID -j DROP
Neue Verbindungen aus öffentlichen Netzen zu privaten Docker-Netzen werden anschließend blockiert und protokolliert. Die vollständigen Regeln und ihre Funktionsweise sind in der Projekt-README dokumentiert.
Die Installation über das mitgelieferte Skript ist jedoch komfortabler, da:
- vorhandene Dateien gesichert werden,
- IPv4 und IPv6 berücksichtigt werden,
- individuelle Docker-Subnetze unterstützt werden,
- Regeln für einzelne Container automatisch erzeugt werden,
- ein systemd-Dienst eingerichtet werden kann,
- veraltete Container-IP-Adressen aktualisiert werden können.
ufw-docker deinstallieren
Die Installation kann mit folgendem Befehl entfernt werden:
sudo ufw-docker uninstall
Danach sollte UFW neu gestartet werden:
sudo systemctl restart ufw
Das Skript entfernt die von ufw-docker eingefügten Regelblöcke aus den IPv4- und IPv6-Konfigurationsdateien, entfernt die installierte Programmdatei, die Manpage und den systemd-Dienst. Vor Änderungen werden Sicherungskopien der UFW-Dateien angelegt.
Typische Fehler
Der Port ist weiterhin öffentlich erreichbar
Zunächst sollte geprüft werden, ob die Regeln installiert wurden:
sudo ufw-docker check
Danach:
sudo iptables -L DOCKER-USER -n -v
Falls die Änderungen nach einem UFW-Neustart nicht greifen, sollte der Server vollständig neu gestartet werden:
sudo reboot
Der falsche Port wurde freigegeben
Bei:
ports:
- "8080:80"
muss Port 80 angegeben werden:
sudo ufw-docker allow containername 80/tcp
Nicht Port 8080.
Die Regel verweist auf eine alte Container-IP
Nach einer Neuerstellung:
sudo ufw-docker reload
Oder die Regel entfernen und neu anlegen:
sudo ufw-docker delete allow containername 80/tcp
sudo ufw-docker allow containername 80/tcp
SSH ist nach Aktivierung von UFW nicht mehr erreichbar
Vor Aktivierung der Firewall muss der SSH-Port freigegeben werden:
sudo ufw allow OpenSSH
Bei einem individuellen SSH-Port, beispielsweise 2222:
sudo ufw allow 2222/tcp
Ein internes Netz wird blockiert
Das benötigte Netz kann bei der Installation explizit angegeben werden:
sudo ufw-docker install \
--docker-subnets \
172.20.0.0/16 \
10.10.10.0/24 \
fd00:20::/64 \
--system
Dabei sollten nur tatsächlich vertrauenswürdige Netze aufgenommen werden.
Empfohlene Basiskonfiguration
Für einen Server mit SSH und einem Traefik-Reverse-Proxy ergibt sich beispielsweise folgende Konfiguration:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw default deny routed
sudo ufw allow 22/tcp
sudo ufw enable
cd /opt
sudo git clone https://github.com/chaifeng/ufw-docker.git
cd ufw-docker
sudo chmod +x ufw-docker
sudo ./ufw-docker install --docker-subnets --system
sudo systemctl restart ufw
sudo reboot
Nach dem Neustart:
sudo ufw-docker allow traefik 80/tcp
sudo ufw-docker allow traefik 443/tcp
Überprüfung:
sudo ufw status numbered
sudo ufw-docker list
sudo ufw-docker check
Fazit
Die Kombination aus Docker und UFW kann ein falsches Sicherheitsgefühl vermitteln. Ein mit Docker veröffentlichter Port ist nicht automatisch durch die normalen UFW-Eingangsregeln geschützt.
ufw-docker löst dieses Problem, indem es UFW mit der Docker-Kette DOCKER-USER verbindet. Dadurch können veröffentlichte Container-Ports zentral über UFW gesteuert werden, während Docker seine Netzwerk- und NAT-Regeln weiterhin selbst verwaltet.
Für öffentlich erreichbare Docker-Server ist das besonders sinnvoll:
- Standardmäßig bleiben Container-Ports geschlossen.
- Nur benötigte Dienste werden gezielt freigegeben.
- Reverse Proxies wie Traefik können als einziger Einstiegspunkt dienen.
- Interne Dienste bleiben in privaten Docker-Netzwerken.
- IPv4 und IPv6 können gemeinsam abgesichert werden.
- Änderungen lassen sich mit bekannten UFW-Befehlen kontrollieren.
Trotzdem ersetzt ufw-docker keine vollständige Sicherheitsstrategie. Container sollten regelmäßig aktualisiert, unnötige Portfreigaben vermieden und öffentlich erreichbare Dienste zusätzlich durch TLS, Authentifizierung, Rate-Limits und Sicherheitssoftware geschützt werden.
Weiterführende Links
- ufw-docker auf GitHub:
https://github.com/chaifeng/ufw-docker - Docker-Dokumentation zur Paketfilterung:
https://docs.docker.com/engine/network/packet-filtering-firewalls/ - Ubuntu-Dokumentation zu UFW:
https://documentation.ubuntu.com/server/how-to/security/firewalls/
