Zum Inhalt springen
SHIELDMYSERVER

Docker umgeht deine Firewall und was dagegen hilft

Warum ufw den Port als geschlossen meldet, während die Datenbank aus dem Internet erreichbar ist, wie die DOCKER-USER-Kette funktioniert und warum lokales Binden die bessere Lösung ist.

hochDiese Woche. Deutlich erhöhtes Risiko, wenn es liegen bleibt.

veröffentlicht 30.07.2026 · 2 min

Titelbild: Docker umgeht deine Firewall und was dagegen hilft

Der Ablauf ist immer derselbe: ufw status meldet Status: active, Port 5432 taucht nirgends auf, und trotzdem lässt sich die Datenbank von außen erreichen. Kein Konfigurationsfehler, so ist Docker gebaut.

Warum hoch

Die Lücke ist besonders unangenehm, weil sie mit einem falschen Sicherheitsgefühl einhergeht. Wer ufw status prüft und beruhigt weitergeht, hat eine offene Datenbank und einen Beleg dafür, dass alles zu ist.

Warum das passiert#

Docker richtet beim Veröffentlichen eines Ports eine DNAT-Regel in der nat-Tabelle ein. Diese greift, bevor die filter-Regeln von ufw drankommen, der Verkehr ist also schon umgeleitet, wenn ufw ihn sehen würde.

Sichtbar wird es so:

# Was ufw meint
sudo ufw status verbose

# Was tatsächlich gilt
sudo iptables -t nat -L DOCKER -n --line-numbers
sudo iptables -L DOCKER -n | head -20

Und die Gegenprobe von außen, von einer anderen Maschine:

nmap -Pn -p 5432,6379,9090 203.0.113.10

Die gute Lösung: gar nicht erst veröffentlichen#

Der Reflex, gegen Docker anzukämpfen, führt in die Irre. Der eigentliche Fehler steckt meist in der Compose-Datei.

services:
  db:
    image: postgres:17.4-alpine
    # falsch: bindet auf allen Adressen
    # ports: ["5432:5432"]
    #
    # richtig, wenn Zugriff von außerhalb des Compose-Netzes nötig ist:
    ports: ["127.0.0.1:5432:5432"]
    networks: [intern]

  app:
    image: ghcr.io/beispiel/app:1.8.2
    # erreicht die Datenbank über den Dienstnamen, ohne veröffentlichten Port
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORT}@db:5432/app
    ports: ["127.0.0.1:3000:3000"]
    networks: [intern, web]

networks:
  intern:
    internal: true      # kein Weg ins Internet
  web:

Drei Regeln, die das Problem an der Wurzel lösen:

  1. Was nur intern gebraucht wird, bekommt gar kein ports. Container im selben Netz erreichen sich über den Dienstnamen.
  2. Was lokal erreichbar sein muss, bindet an 127.0.0.1.
  3. Was von außen erreichbar sein muss, steht hinter einem Reverse-Proxy, dann ist genau ein Port offen und TLS liegt an einer Stelle.
# Kontrolle: keine Zeile darf 0.0.0.0 enthalten
docker compose ps --format 'table {{.Service}}\t{{.Ports}}'

Wenn es doch eine Firewallregel braucht#

Für Fälle, in denen ein Container auf allen Adressen hören muss, aber nur bestimmte Quellen erlaubt sind, gibt es die Kette DOCKER-USER. Sie wird vor den Docker-eigenen Regeln ausgewertet und von Docker nicht überschrieben.

# Nur aus dem VPN-Netz auf den veröffentlichten Port 5432
sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport 5432 -s 10.99.0.0/24 -j ACCEPT
sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport 5432 -j DROP

Mit nftables entsprechend:

table ip filter {
    chain DOCKER-USER {
        iifname "eth0" tcp dport 5432 ip saddr 10.99.0.0/24 accept
        iifname "eth0" tcp dport 5432 drop
    }
}
Regeln überleben keinen Neustart

Von Hand eingefügte DOCKER-USER-Regeln sind nach einem Neustart weg. Sie gehören in ein Regelwerk, das beim Booten geladen wird (iptables-persistent oder die nftables-Konfiguration), sonst ist der Schutz genau so lange aktiv, bis jemand die Maschine neu startet, und danach fällt es niemandem auf.

IPv6 nicht vergessen#

Docker aktiviert IPv6 nicht standardmäßig. Wo es aktiviert ist, gilt dasselbe Problem noch einmal, mit eigenen Tabellen:

// /etc/docker/daemon.json
{
  "ipv6": true,
  "fixed-cidr-v6": "2001:db8:1::/64",
  "ip6tables": true
}

Ohne "ip6tables": true gibt es für IPv6 überhaupt keine Docker-Regeln, veröffentlichte Ports sind dann ungefiltert erreichbar. Nach jeder Änderung von außen gegenprüfen, nicht nur die Konfiguration lesen.

Der Docker-Socket#

Ein verwandtes Thema, das oft im selben Atemzug auftaucht: Jeder Container mit Zugriff auf /var/run/docker.sock kann einen privilegierten Container starten und damit den Host übernehmen. :ro ändert daran nichts, der Socket ist eine API, keine Datei mit Rechten.

# Welche Container haben den Socket eingehängt?
docker ps -q | xargs docker inspect --format \
  '{{.Name}}: {{range .Mounts}}{{.Source}} {{end}}' | grep docker.sock

Wo es wirklich nötig ist (Monitoring, Update-Melder), gehört ein Socket-Proxy davor, der nur Leseoperationen durchlässt.

Zusammengefasst#

SymptomUrsacheLösung
Port offen trotz ufwDNAT vor filternicht veröffentlichen oder 127.0.0.1 binden
Regel wirkt nichtfalsche KetteDOCKER-USER statt INPUT
Nach Neustart wieder offenRegel nicht persistentins Boot-Regelwerk
Nur IPv6 erreichbarip6tables ausin daemon.json aktivieren

Die Grundlagen der eingehenden Filterung stehen in Eingehend dichtmachen; wie ein sauberer Compose-Aufbau insgesamt aussieht, steht auf der Schwesterseite unter Docker im echten Betrieb.

MRMedia

Geschrieben aus dem laufenden Betrieb: MRMedia betreibt eigene Hosts, Kundendienste und deren Härtung in der EU. Eine veraltete Anleitung ist hier ein Sicherheitsproblem, kein Schönheitsfehler. Korrekturen bitte über Discord.