Lese-Ansicht

Dockers nft-Inkompatibilität wird zunehmend zum Ärgernis

Ca. seit 2020 kommt nftables (Kommando nft) per Default als Firewall-Backend unter Linux zum Einsatz. Manche Distributionen machten den Schritt noch früher, andere folgten ein, zwei Jahre später. Aber mittlerweile verwenden praktisch alle Linux-Distributionen nftables.

Alte Firewall-Scripts mit iptables funktionieren dank einer Kompatibilitätsschicht zum Glück größtenteils weiterhin. Viele wichtige Firewall-Tools und -Anwendungen (von firewalld über fail2ban bis hin zu den libvirt-Bibliotheken) brauchen diese Komaptibilitätsschicht aber nicht mehr, sondern wurden auf nftables umgestellt.

Welches Programm ist säumig? Docker! Und das wird zunehmend zum Problem.

Update 11.11.2025: In naher Zukunft wird mit Engine Version 29.0 nftables als experimentelles Firewall-Backend ausgeliefert. (Aktuell gibt es den rc3, den ich aber nicht getestet habe. Meine lokalen Installationen verwenden die Engine-Version 28.5.1.) Ich habe den Artikel diesbezüglich erweitert/korrigiert. Sobald die Engine 29 ausgeliefert wird, werde ich das neue Backend ausprobieren und einen neuen Artikel verfassen.

Docker versus libvirt/virt-manager

Auf die bisher massivsten Schwierigkeiten bin ich unter Fedora >=42 und openSUSE >= 16 gestoßen: Wird zuerst Docker installiert und ausgeführt, funktioniert in virtuellen Maschinen, die mit libvirt/virt-manager gestartet werden, das NAT-Networking nicht mehr. Und es bedarf wirklich einiger Mühe, den Zusammenhang mit Docker zu erkennen. Die vorgebliche »Lösung« besteht darin, die libvirt-Firewall-Funktionen von nftables zurück auf iptables zu stellen. Eine echte Lösung wäre es, wenn Docker endlich nftables unterstützen würde.

# in der Datei /etc/libvirt/network.conf
firewall_backend = "iptables"

Danach starten Sie den libvirt-Dämon dann neu:

sudo systemctl restart libvirtd

Weitere Infos gibt es hier und hier. Die openSUSE-Release-Notes weisen ebenfalls auf das Problem hin.

Sicherheitsprobleme durch offene Ports

Weil Docker iptables verwendet, ist es mit nftables oder firewalld nicht möglich, Container mit offenen Ports nach außen hin zu blockieren. Wenn Sie also docker run -p 8080:80 machen oder in compose.yaml eine entsprechende Ports-Zeile einbauen, ist der Port 8080 nicht nur auf dem lokalen Rechner sichtbar, sondern für die ganze Welt! nftables- oder firewalld-Regeln können dagegen nichts tun!

Deswegen ist es wichtig, dass Docker-Container möglichst in internen Netzwerken miteinander kommunizieren bzw. dass offene Ports unbedingt auf localhost limitiert werden:

# Port 8080 ist nur für localhost zugänglich.
docker run -p localhost:8080:80 ...

Ihre Optionen in compose.yaml sehen so aus:

```bash
# compose.yaml
# Ziel: myservice soll mit anderem Container
# in compose.yaml über Port 8888 kommunizieren
services:
  myservice:
    image: xxx
    ports:
      # unsicher!
      # - "8888:8888"
      # besser (Port ist für localhost sichtbar)
      # - "127.0.0.1:8888:8888"
      # noch besser (Port ist nur Docker-intern offen)
      - "8888"
  otherservice:
    ...

Die sicherste Lösung besteht darin, die Container ausschließlich über ein Docker-internes Netzwerk miteinander zu verbinden (siehe backend_network im folgenden schablonenhaften Beispiel). Die Verbindung nach außen für die Ports 80 und 443 erfolgt über ein zweites Netzwerk (frontend_network). Die Angabe des drivers ist optional und verdeutlicht hier nur den Default-Netzwerktyp.

# compose.yaml
# am besten: die beiden Services myservice und
# nginx kommunizieren über das interne Netzwerk 
# miteinander
services:
  myservice:
    build: .
    ports:
      - "8888:8888"
    networks:
     - backend_network
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - /etc/mycerts/fullchain.pem:/etc/nginx/ssl/nginx.crt
      - /etc/mycerts/privkey.pem:/etc/nginx/ssl/nginx.key
    depends_on:
      - myservice
    networks:
      - frontend_network
      - backend_network
networks:
  # Verbindung zum Host über eine Bridge
  frontend_network:
    driver:   bridge
  # Docker-interne Kommunikation zwischen den Containern
  backend_network:
    driver:   bridge
    internal: true

Restriktive Empfehlungen

Docker hat es sich zuletzt sehr einfach gemacht. Auf https://docs.docker.com/engine/install/ubuntu/#firewall-limitations steht:

If you use ufw or firewalld to manage firewall settings, be aware that when you expose container ports using Docker, these ports bypass your firewall rules. For more information, refer to Docker and ufw.

Docker is only compatible with iptables-nft and iptables-legacy. Firewall rules created with nft are not supported on a system with Docker installed. Make sure that any firewall rulesets you use are created with iptables or ip6tables, and that you add them to the DOCKER-USER chain, see Packet filtering and firewalls.

Und https://docs.docker.com/engine/security/#docker-daemon-attack-surface gibt diese Zusammenfassung:

Finally, if you run Docker on a server, it is recommended to run exclusively Docker on the server, and move all other services within containers controlled by Docker. Of course, it is fine to keep your favorite admin tools (probably at least an SSH server), as well as existing monitoring/supervision processes, such as NRPE and collectd.

Salopp formuliert: Verwenden Sie für das Docker-Deployment ausschließlich für diesen Zweck dezidierte Server und/oder verwenden Sie bei Bedarf veraltete iptable-Firewalls. Vor fünf Jahren war dieser Standpunkt noch verständlich, aber heute geht das einfach gar nicht mehr. Die Praxis sieht ganz oft so aus, dass auf einem Server diverse »normale« Dienste laufen und zusätzlich ein, zwei Docker-Container Zusatzfunktionen zur Verfügung stellen. Sicherheitstechnisch wird dieser alltägliche Wunsch zum Alptraum.

Land in Sicht?

Bei Docker weiß man natürlich auch, dass iptables keine Zukunft hat. Laut diesem Issue sind 10 von 11 Punkte für die Umstellung von iptables auf nftables erledigt. Aber auch dann ist unklar, wie es weiter gehen soll: Natürlich ist das ein massiver Eingriff in grundlegende Docker-Funktionen. Die sollten vor einem Release ordentlich getestet werden. Einen (offiziellen) Zeitplan für den Umstieg auf nftables habe ich vergeblich gesucht.

Docker ist als Plattform-überschreitende Containerlösung für Software-Entwickler fast konkurrenzlos. Aber sobald man den Sichtwinkel auf Linux reduziert und sich womöglich auf Red-Hat-ähnliche Distributionen fokussiert, sieht die Lage anders aus: Podman ist vielleicht nicht hundertprozentig kompatibel, aber es ist mittlerweile ein sehr ausgereiftes Container-System, das mit Docker in vielerlei Hinsicht mithalten kann. Installationsprobleme entfallen, weil Podman per Default installiert ist. Firewall-Probleme entfallen auch. Und der root-less-Ansatz von Podman ist sicherheitstechnis sowieso ein großer Vorteil (auch wenn er oft zu Netzwerkeinschränkungen und Kompatibilitätsproblemen führt, vor allem bei compose-Setups).

Für mich persönlich war Docker immer die Referenz und Podman die nicht ganz perfekte Alternative. Aber die anhaltenden Firewall-Probleme lassen mich an diesem Standpunkt zweifeln. Die Firewall-Inkompatibilität ist definitiv ein gewichtiger Grund, der gegen den Einsatz der Docker Engine auf Server-Installationen spricht. Docker wäre gut beraten, iptables ENDLICH hinter sich zu lassen!

Update 11.11.2025: Als ich diesen Artikel im Oktober 2025 verfasst und im November veröffentlicht habe, ist mir entgangen, dass die Docker-Engine in der noch nicht ausgelieferten Version 29 tatsächlich bereits ein experimentelles nftables-Backend enthält!

Version 29 liegt aktuell als Release Candidate 3 vor. Ich warte mit meinen Tests, bis die Version tatsächlich ausgeliefert wird. Hier sind die Release Notes, hier die neue Dokumentationsseite. Vermutlich wird es ein, zwei weitere Releases brauchen, bis das nftables-Backend den Sprung von »experimentell« bis »stabil« schafft, aber immerhin ist jetzt ganz konkret ein Ende der Firewall-Misere in Sicht.

Quellen / Links

Das neue nftables-Backend für Docker

  •  

Container mit podman-auto-update automatisch aktualisieren

In diesem Tutorial zeige ich euch, wie ihr eine automatische Aktualisierung für Container in rootless-Podman-Umgebungen konfigurieren und diese Container als systemd-Services verwalten könnt.

Das Tutorial gliedert sich in folgende Abschnitte:

  1. Anwendungsfälle
  2. Voraussetzungen
  3. Umgebung und verwendetes Container-Image
  4. Konfiguration des systemd-Service mit Auto-Update-Funktion
  5. Container (automatisch) aktualisieren

Wer sich nicht für die möglichen Anwendungsfälle interessiert und lieber gleich starten möchte, kann den ersten Abschnitt überspringen. Die übrigen Abschnitte sollten in der angegebenen Reihenfolge gelesen werden.

Anwendungsfälle

  • Container werden auf einem Single-Container-Host ausgeführt und nicht in K8s-Umgebungen
  • Man vertraut dem Anbieter, dass dieser stabile und nutzbare Container-Images bereitstellt
  • Es soll regelmäßig geprüft werden, ob aktualisierte Container-Images vorhanden sind
  • Sind aktuellere Images vorhanden, sollen laufende Container entfernt und unter Verwendung der aktuellen Images neu erstellt werden

Voraussetzungen

Um diesem Tutorial folgen zu können, benötigt ihr einen Host mit einer rootless-Podman-Umgebung. Podman muss dabei in der Version >= 3.3 verfügbar sein. In der folgenden Liste findet ihr einige Links, die euch helfen, eine solche Umgebung aufzusetzen.

Darüber hinaus solltet ihr Manpages lesen können.

Umgebung und verwendetes Container-Image

Das Betriebssystem spielt eine untergeordnete Rolle, da wir durch die Verwendung von Containern die Anwendung vom Betriebssystem entkoppeln. Alle zur Ausführung der Anwendung notwendigen Abhängigkeiten sind im Container-Image enthalten.

Uptime Kuma ist eine schlanke und schnelle Monitoring-Anwendung, welche unter anderem als Container-Image bereitgestellt wird. Ich habe die Anwendung als Beispiel für dieses Tutorial ausgewählt, da ich die Anwendung selbst nutzen möchte und so Synergieeffekte nutzen kann.

Wer ein anderes Container-Image nutzen möchte, muss in den folgenden Beispielen louislam/uptime-kuma:latest durch den fully qualified container name des zu nutzenden Images ersetzen.

Für die Konfiguration werden die auf dem System verfügbaren Podman-Manpages benutzt.

Konfiguration des Systemd-Service mit Auto-Update-Funktion

Bei den folgenden Schritten habe ich mich am Beispiel aus podman-auto-update(1) orientiert. Ich zeige zuerst den jeweils auszuführenden Befehl in einem Code-Block, gefolgt von einer Erläuterung der genutzten Optionen.

Podman-Volume erzeugen

$ podman volume create uptime-kuma
uptime-kuma

Um Daten persistent speichern zu können, müssen diese außerhalb des Containers abgelegt werden. Der dargestellte Befehl erzeugt ein Podman-Volume mit dem Namen „uptime-kuma“.

Mit dem folgenden Befehl lassen sich detaillierte Informationen zum gerade erstellten Volume anzeigen:

$ podman volume inspect uptime-kuma
[
     {
          "Name": "uptime-kuma",
          "Driver": "local",
          "Mountpoint": "/home/tronde/.local/share/containers/storage/volumes/uptime-kuma/_data",
          "CreatedAt": "2023-08-22T20:52:06.477341481+02:00",
          "Labels": {},
          "Scope": "local",
          "Options": {},
          "MountCount": 0,
          "NeedsCopyUp": true,
          "NeedsChown": true
     }
]

Der Schlüssel Mountpoint enthält den Pfad im lokalen Dateisystem, in dem das Volume erstellt wurde.

Manpages zum Nachschlagen:

  • podman-volume(1)
  • podman-volume-create(1)
  • podman-volume-inspect(1)

Container starten

$ podman run --label "io.containers.autoupdate=registry" -d -p 3001:3001 -v uptime-kuma:/app/data:Z --name=uptime-kuma docker.io/louislam/uptime-kuma:latest
Trying to pull docker.io/louislam/uptime-kuma:latest...
Getting image source signatures
Copying blob d7ca72974892 done  
Copying blob 475646a04a73 done  
Copying blob 1d496c24aec8 done  
Copying blob 6a3727d8681d done  
Copying blob b00c91ba9805 done  
Copying blob ddade83992f9 done  
Copying blob b8454ed537a7 done  
Copying blob 849e609eff67 done  
Copying blob f861188db6a1 done  
Copying blob 5f3c3e6d7f1c done  
Copying blob 4f4fb700ef54 skipped: already exists  
Copying blob 68b7bcf7c878 done  
Copying config fb3a3565b2 done  
Writing manifest to image destination
Storing signatures
ad7b049d9b84962311f5bafb5329f59961d8a031e54a571f079b8243ea8059ee
  • podman run ist das Kommando, mit dem ein neuer Container gestartet wird
  • Die Option --label "io.containers.autoupdate=registry" gibt an, dass Podman die Remote-Registry prüft, ob dort ein aktualisiertes Image vorhanden ist; dieses Label ist Voraussetzung, um die Auto-Update-Funktion nutzen zu können
  • Mit der Option -d wird der Container im Hintergrund gestartet und die Container-ID auf STDOUT ausgegeben
  • Durch -p 3001:3001 wird der Host-Port 3001 mit dem Port der Anwendung (ebenfalls 3001) im Container verbunden
  • Die Option -v uptime-kuma:/app/data:Z hängt das im vorhergehenden Schritt erstellte Podman-Volume in das Verzeichnis /app/data innerhalb des Containers ein; :Z sorgt dafür, dass der SELinux-Kontext korrekt gesetzt wird
  • --name=uptime-kuma spezifiziert den Namen des Containers; dieser ist etwas leichter zu merken als die Container-ID
  • Der Befehl endet mit dem fully qualified container name docker.io/louslam/uptime-kuma:latest
  • Die letzte Zeile des Code-Blocks enthält die Container-ID

Manpages zum Nachschlagen:

  • podman-run(1)
  • podman-auto-update(1)

Systemd-Service-Unit mit podman-generate-systemd erstellen

$ podman generate systemd --name --new uptime-kuma
# container-uptime-kuma.service
# autogenerated by Podman 4.4.1
# Tue Aug 22 21:29:46 CEST 2023

[Unit]
Description=Podman container-uptime-kuma.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStart=/usr/bin/podman run \
	--cidfile=%t/%n.ctr-id \
	--cgroups=no-conmon \
	--rm \
	--sdnotify=conmon \
	--replace \
	--label io.containers.autoupdate=registry \
	-d \
	-p 3001:3001 \
	-v uptime-kuma:/app/data:Z \
	--name=uptime-kuma docker.io/louislam/uptime-kuma:latest
ExecStop=/usr/bin/podman stop \
	--ignore -t 10 \
	--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \
	-f \
	--ignore -t 10 \
	--cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target
  • Der Befehl gibt den Inhalt der generierten Service-Unit auf STDOUT aus
  • Die Option --name verwendet den Namen des Containers anstelle der Container-ID im Dateinamen der Service-Unit (hier: container-uptime-kuma.service)
  • Wichtig ist die Option --new, um Container von aktualisierten Images erstellen zu können; ohne diese Option können Systemd-Units Container nur unter Verwendung des ursprünglichen Images starten und stoppen und ein Auto-Update ist nicht möglich
  • Der folgende Code-Block fügt dem Befehl die Option --files hinzu, um eine Service-Unit-Datei zu erstellen
$ podman generate systemd --name --new --files uptime-kuma
/home/tronde/container-uptime-kuma.service

Manpages zum Nachschlagen:

  • podman-auto-update(1)
  • podman-generate-systemd(1)

Die erstellte Systemd-Unit aktivieren und starten

$ mv -Z container-uptime-kuma.service ~/.config/systemd/user/container-uptime-kuma.service
$ systemctl --user daemon-reload
  • Der erste Befehl verschiebt die Service-Unit in einen Pfad, wo systemd sie findet und einlesen kann
  • Die Option -Z stellt sicher, dass die Datei den SELinux-Kontext des Zielverzeichnisses zugewiesen bekommt, andernfalls kann systemd die Datei ggf. nicht verarbeiten
  • Durch den zweiten Befehl wird die Unit-Datei systemd bekannt gemacht
  • An dieser Stelle ist der neue Systemd-Service geladen, jedoch inaktiv
$ systemctl --user status container-uptime-kuma.service
○ container-uptime-kuma.service - Podman container-uptime-kuma.service
     Loaded: loaded (/home/tronde/.config/systemd/user/container-uptime-kuma>
     Active: inactive (dead)
       Docs: man:podman-generate-systemd(1)
$ podman stop uptime-kumauptime-kuma
$ podman rm uptime-kumauptime-kuma
$ systemctl --user start container-uptime-kuma.service
$ systemctl --user status container-uptime-kuma.service
● container-uptime-kuma.service - Podman container-uptime-kuma.service     Loaded: loaded (/home/tronde/.config/systemd/user/container-uptime-kuma>     Active: active (running) since Tue 2023-08-22 21:59:56 CEST; 14s ago
…
  • Der erste Befehl in obigen Code-Block prüft den aktuellen Status des Service
  • Der zweite und dritte Befehl stoppen und entfernen den laufenden Container, den wir weiter oben gestartet haben
  • Befehl Nummer 4 startet den Uptime-Kuma-Service
  • Befehl Nummer 5 prüft den neuen Status des Service; dieser ist nun up-and-running

Manpages zum Nachschlagen:

  • mv(1)
  • systemd.unit(5)
  • systemctl(1)

Auf neue Container-Images prüfen

$ podman auto-update --dry-run --format "{{.Image}} {{.Updated}}"
docker.io/louislam/uptime-kuma:latest false
  • Durch die Option --dry-run wird sichergestellt, dass nur auf die Verfügbarkeit neuer Images geprüft wird, es werden jedoch keine Pull-Operationen ausgeführt und keine Container neu erstellt
  • Es wird eine Liste von Container-Images ausgegeben, die mit dem Label io.containers.autoupdate=registry gestartet wurden
  • Die erste Spalte enthält den Image-Namen
  • Die zweite Splate zeigt an, ob ein Update verfügbar ist; in diesem Fall ist kein Update verfügbar (false)

Container (automatisch) aktualisieren

Wurde die Konfiguration erfolgreich abgeschlossen, können die entsprechenden Container durch folgenden Befehl manuell aktualisiert werden:

$ podman auto-update
            UNIT                           CONTAINER                   IMAGE                                  POLICY      UPDATED
            container-uptime-kuma.service  df21116f2573 (uptime-kuma)  docker.io/louislam/uptime-kuma:latest  registry    false

Leider ist aktuell kein Update verfügbar, weshalb es hier nichts zu tun gibt und der Status von Updated gleich false ist.

Podman bringt bei der Installation die beiden systemd units podman-auto-update.timer und podman-auto-update.service mit, welche zumindest unter RHEL 9 manuell aktiviert werden müssen:

$ systemctl --user enable podman-auto-update.{service,timer}
Created symlink /home/tronde/.config/systemd/user/default.target.wants/podman-auto-update.service → /usr/lib/systemd/user/podman-auto-update.service.
Created symlink /home/tronde/.config/systemd/user/timers.target.wants/podman-auto-update.timer → /usr/lib/systemd/user/podman-auto-update.timer.

$ systemctl --user start podman-auto-update.timer
$ systemctl --user status podman-auto-update.{service,timer}
○ podman-auto-update.service - Podman auto-update service
     Loaded: loaded (/usr/lib/systemd/user/podman-auto-update.service; enabled; preset: disabled)
     Active: inactive (dead)
TriggeredBy: ● podman-auto-update.timer
       Docs: man:podman-auto-update(1)

● podman-auto-update.timer - Podman auto-update timer
     Loaded: loaded (/usr/lib/systemd/user/podman-auto-update.timer; enabled; preset: disabled)
     Active: active (waiting) since Sat 2023-09-02 20:56:09 CEST; 1s ago
      Until: Sat 2023-09-02 20:56:09 CEST; 1s ago
    Trigger: Sun 2023-09-03 00:12:22 CEST; 3h 16min left
   Triggers: ● podman-auto-update.service
  • Der Timer startet jeden Tag um Mitternacht den Auto-Update-Service
  • Der Service prüft, ob aktualisierte Container-Images verfügbar sind und führt ggf. ein Update der Container durch
  • Schlägt ein Start nach Aktualisierung des Container-Images fehl, wird der Dienst automatisch von der vorherigen Image-Version gestartet; siehe --rollback in podman-auto-update(1)
  • Der folgende Code-Block zeigt den Status, nachdem ein Update durchgeführt wurde
$ systemctl --user --no-pager -l status podman-auto-update
○ podman-auto-update.service - Podman auto-update service
     Loaded: loaded (/usr/lib/systemd/user/podman-auto-update.service; enabled; preset: disabled)
     Active: inactive (dead) since Sun 2023-09-03 00:12:56 CEST; 7h ago
TriggeredBy: ● podman-auto-update.timer
       Docs: man:podman-auto-update(1)
    Process: 309875 ExecStart=/usr/bin/podman auto-update (code=exited, status=0/SUCCESS)
    Process: 310009 ExecStartPost=/usr/bin/podman image prune -f (code=exited, status=0/SUCCESS)
   Main PID: 309875 (code=exited, status=0/SUCCESS)
        CPU: 5.128s

Sep 03 00:12:50 example.com podman[309875]: Copying config sha256:d56b643e048f2d351ed536ec9a588555dfd4c70de3c8d510ed61078a499ba464
Sep 03 00:12:50 example.com podman[309875]: Writing manifest to image destination
Sep 03 00:12:50 example.com podman[309875]: Storing signatures
Sep 03 00:12:51 example.com podman[309875]: 2023-09-03 00:12:41.98296115 +0200 CEST m=+1.880671312 image pull  docker.io/louislam/uptime-kuma:latest
Sep 03 00:12:55 example.com podman[309875]:             UNIT                           CONTAINER                   IMAGE                                  POLICY      UPDATED
Sep 03 00:12:55 example.com podman[309875]:             container-uptime-kuma.service  814407c7312c (uptime-kuma)  docker.io/louislam/uptime-kuma:latest  registry    true
Sep 03 00:12:56 example.com podman[310009]: fb3a3565b2da641402e99594e09b3cdadd1b9aa84f59e7960b9961662da5ff65
Sep 03 00:12:56 example.com podman[310009]: 2023-09-03 00:12:55.421998943 +0200 CEST m=+0.020260134 image remove fb3a3565b2da641402e99594e09b3cdadd1b9aa84f59e7960b9961662da5ff65 
Sep 03 00:12:56 example.com systemd[686]: Finished Podman auto-update service.
Sep 03 00:12:56 example.com systemd[686]: podman-auto-update.service: Consumed 5.128s CPU time.

Ich hoffe, das Tutorial hat euch gefallen und konnte euch einen Eindruck vermitteln, wie man automatische Updates für Container konfigurieren kann.

  •