In diesem Blogpost soll aufgezeigt werden, inwiefern Container iptables/nftables benötigen und in welchen Anwendungsfällen diese Technologien wiederum ein Problem für Containers darstellen können. In den nächsten beiden Blogposts erfahrt ihr mehr über netfilter & Docker Daemon und Podman sowie netfilter & Kubernetes.

Netfilter: nftables vs. iptables
Bevor in die eigentliche Thematik zu Containern und iptables/nftables eingegangen werden kann, würde ich gerne erst ganz grob die auf die Unterschiede dieser beiden “netfiltering” Technologien eingehen.
Grundsätzlich kann gesagt werden, dass nftables die Ablösung vom legacy iptables ist und das beide auf dem gleichen Linux Kernel-Framework – netfilter – aufbauen. Nftables probiert dabei viele Unschönheiten von iptables wie z.B. fehlende Skalierbarkeit, schlechte Performance und Komplexität bei der Konfiguration zu verbessern respektive zu vereinfachen. Netfilter selbst bietet diverse Funktionen wie Packet Filtering, Network Address Translation (NAT), Port Address Translation (PAT), Routing von Netzwerk-Paketen und Isolierung von einzelnen Netzwerk-Segmenten an. Diese Funktionalitäten können via Kernel Hooks von Linux Kernel Modulen wie z.B. eben ip_tables
oder nf_tables
verwendet werden, indem sie Callback Funktionen bei diesen Hooks registrieren. Sobald nun ein Netzwerk-Paket den Netzwerk-Stack des Kernels durchlaufen will, werden die registrierten Funktionen aufgerufen und diese haben somit nun die Chance, das Paket nach ihrer vordefinierten Konfiguration zu validieren, zu bearbeiten oder gar zu verwerfen.
Obwohl der Übergang von iptables zu nftables bei den netfilter Entwicklern schon seit längerem vorgesehen ist, befinden wir uns noch mittendrin. Aufgrund diverser Abhängigkeiten und damit verbundenen Schwierigkeiten wird die Umstellung bei diversen Linux Distibutionen erst noch geplant oder wurde erst im letzten Major Release durchgeführt. Beispiele hierfür sind z.B. Red Hat mit dem Upgrade von der Version 7 auf 8 oder Debian mit dem neuen Release 10 (Buster). Genau aus diesem Grund wurde auch eine Art von Zwischenstufe mit iptables-nft
entwickelt. iptables-nft
erlaubt es nämlich, dass die bestehende legacy iptables Konfigurationssyntax via nftables API angewendet werden kann. Für das Packet Matching selbst wird dann zwar weiterhin das xtables Kernel-Konstrukt verwendet (diagonaler Pfeil in der Grafik), jedoch werden die legacy iptables Rules dann auch in die neue nftables Konfigurationssyntax umgewandelt und sie ist anschliessend auch über die Ausgabe der nftables Rules sichtbar.

Wer einfach nur eine Host Firewall auf seinen Linux Systemen haben will, ohne sich um iptables oder nftables Details zu kümmern, ist mit FirewallD gut bedient. FirewallD ist im Grunde nichts anderes als ein Frontend, welches verschiedene Backends haben kann. Je nach FirewallD Version kann zwischen einem iptables oder nftables Backend frei gewählt werden. Seit der FirewallD Version 0.7.0 wird nftables als vollwertiges Backend unterstützt. In der FirewallD Version 0.8.0 und neuer wurde dieser nftables Support sogar noch weiter ausgebaut und vor allem verbessert. Neu spricht FirewallD dann das nft
Binary selbst nicht mehr an, sondern schreibt die Rules direkt via libnftnl
in den Kernel Space.
Auf eine genauere Gegenüberstellungen von iptables und nftables wird ab dieser Stelle verzichtet, da dies nicht das Hauptziel dieses Blogposts sein soll. Wer gerne mehr über die Unterschiede erfahren will, kann diese gerne bei z.B. den folgenden Blogposts/Artikeln nachlesen:
- https://ungleich.ch/en-us/cms/blog/2018/08/18/iptables-vs-nftables/#
- https://developers.redhat.com/blog/2020/08/18/iptables-the-two-variants-and-their-relationship-with-nftables/
- https://en.wikipedia.org/wiki/Netfilter
Netfilter (iptables) & Docker Daemon
In den letzten Jahren bin ich immer wieder in die Problematik reingelaufen, dass sich manuelle iptables / nftables Rules nicht mit auf dem Host laufenden Docker Containers verstanden haben. D.h. in konkreten, einfacheren Fällen war es z.B. dann so, dass der Docker Daemon mit seinem (per Default aktiviertem) iptables Rule-Handling die manuellen iptables Rules umging, sodass ein Service innerhalb eines Docker Containers vom Host LAN aus erreichbar war, obwohl das laut manuellen Host (INPUT
Chain) iptables Rules eigentlich nicht hätte der Fall sein dürfen. Der Docker Daemon arbeitet mit einer eigenen DOCKER
und DOCKER-USER
iptables Chain, welche in der FORWARD
Chain an unterschiedlichen Stellen angezogen werden. Hier ein kleines Beispiel, welches nur die für diesen Anwendungsfall relevanten iptables Rules beinhaltet:
# Wir erlauben einkommend auf den Host ausschliesslich SSH (22/TCP): root@ubuntu-s-1vcpu-1gb-fra1-01:~# iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT root@ubuntu-s-1vcpu-1gb-fra1-01:~# iptables -A INPUT -j DROP # Unser Node soll per Design eigentlich keine Pakete weiterleiten dürfen: root@ubuntu-s-1vcpu-1gb-fra1-01:~# iptables -A FORWARD -j DROP # Ausgehend werden keine neue Verbindungen erlaubt. Ausschliesslich Responses auf bestehende Verbindungen dürfen verschickt werden: root@ubuntu-s-1vcpu-1gb-fra1-01:~# iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT root@ubuntu-s-1vcpu-1gb-fra1-01:~# iptables -A OUTPUT -j DROP # Ein Dummy Apache2 Webserver wird auf dem Host Port 8080 gestartet: root@ubuntu-s-1vcpu-1gb-fra1-01:~# docker run -d --rm -p 8080:80 httpd root@ubuntu-s-1vcpu-1gb-fra1-01:~# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 57e2364b9f07 httpd "httpd-foreground" 5 seconds ago Up 3 seconds 0.0.0.0:8080->80/tcp gallant_carver # Übersicht über die aktuell vorhandenen iptables "filter" Rules: root@ubuntu-s-1vcpu-1gb-fra1-01:~# iptables -v -L -n -t filter Chain INPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 42 2499 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 ctstate NEW,ESTABLISHED 44 9495 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 Chain FORWARD (policy DROP 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 0 0 DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0 0 0 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED 0 0 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0 0 0 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0 0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0 0 0 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 24 3396 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate ESTABLISHED 0 0 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 Chain DOCKER (1 references) pkts bytes target prot opt in out source destination 0 0 ACCEPT tcp -- !docker0 docker0 0.0.0.0/0 172.17.0.2 tcp dpt:80 Chain DOCKER-USER (1 references) pkts bytes target prot opt in out source destination 0 0 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 # Node IP raussuchen: root@ubuntu-s-1vcpu-1gb-fra1-01:~# ip -br -f inet address show dev eth0 eth0 UP 167.71.43.74/20 10.19.0.5/16 # Zugriff von Extern testen (Entwickler Notebook): user@notebook:/tmp$ curl <html><body><h1>It works!</h1></body></html> # Übersicht über die "pkts"/"bytes" Counters der Rules nach dem Zugriff: root@ubuntu-s-1vcpu-1gb-fra1-01:~# iptables -v -L -n -t filter Chain INPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 62 3470 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 ctstate NEW,ESTABLISHED 90 17157 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 Chain FORWARD (policy DROP 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 9 735 DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0 4 241 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED 1 52 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0 4 442 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0 0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0 0 0 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 42 10509 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate ESTABLISHED 0 0 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 Chain DOCKER (1 references) pkts bytes target prot opt in out source destination 1 52 ACCEPT tcp -- !docker0 docker0 0.0.0.0/0 172.17.0.2 tcp dpt:80 # <- "docker run ... -p 8080:80" Chain DOCKER-USER (1 references) pkts bytes target prot opt in out source destination 9 735 RETURN all -- * * 0.0.0.0/0
Wie anhand dieses Beispieles nachvollzogen werden kann, scheint die INPUT
Chain für Docker Services selbst überhaupt keine Relevanz zu haben, da unser Verbindungsversuch vom Entwickler Notebook aus ohne Probleme funktionierte. Dies lässt sich dadurch schlüssig erklären, indem genauer betrachtet wird, wie das Default Docker Container Networking abläuft. In der Realität ist nämlich jeder Docker Container für den Host lediglich ein weiterer virtueller “Host”, welcher über die virtuelle docker0
Linux Bridge (Default) mit der Host NIC verbunden ist:

Aus diesem Kontext lässt sich nun entnehmen, wieso die Host INPUT
iptables Chain keinen Einfluss auf die Erreichbarkeit der published Docker Container Services hat: Der Traffic wird ausschliesslich von der FOWARD
Chain behandelt, da er für den Container und nicht den Host selbst gedacht ist. Es handelt sich hierbei also um ein “FORWARD
ing” (Routing) – deshalb auch das Router Symbol in der Grafik oben.
Der Eine oder die Andere fragt sich nun bestimmt, wieso die automatisch vom Docker Daemon angelegte DOCKER
Chain Rule für tcp dpt:80
und nicht für 8080
gilt – schliesslich haben wir im Beispiel ja den Port 8080
auf Host Ebene gewählt und entsprechend freigegeben. Nun, dies kommt daher, da vor dem Filtering selbst via PREROUTING
noch ein Destination NAT (DNAT
) stattfindet, welches den Destination Port (8080
) vom Request Paket auf den Destination Port (80
) des Containers umbiegt. Das Gleiche wird auch für die Destination IP Adresse gemacht, da die private IPv4 Adresse vom Container (im Beispiel unten 172.17.0.2
) dem Entwickler-Notebook nicht bekannt sein kann und auch die Antwort vom Container zurück, könnte im Internet so nicht gerouted werden, da dann die Source IP Adresse aus dem privaten IPv4 Adressbereich kommen würde (RFC1918):
root@ubuntu-s-1vcpu-1gb-fra1-01:~# iptables -v -L -n -t nat Chain PREROUTING (policy ACCEPT 1118 packets, 106K bytes) pkts bytes target prot opt in out source destination 1142 107K DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL Chain INPUT (policy ACCEPT 30 packets, 1800 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 4 packets, 304 bytes) pkts bytes target prot opt in out source destination 0 0 DOCKER all -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL Chain POSTROUTING (policy ACCEPT 18 packets, 964 bytes) pkts bytes target prot opt in out source destination 0 0 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0 0 0 MASQUERADE tcp -- * * 172.17.0.2 172.17.0.2 tcp dpt:80 Chain DOCKER (2 references) pkts bytes target prot opt in out source destination 0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0 18 964 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80 # <- "docker run ... -p 8080:80"
Da der Docker Daemon seine Rules allesamt in der DOCKER
Chain verwaltet und zugleich schaut, dass seine benötigten Einträge innerhalb der FORWARD
Chain immer zuoberst sind, hätte man mit manuellen FORWARD
Chain Einträgen keine Chance was Sinniges hinzubekommen. Genau für diesen Zweck verlinkt der Docker Daemon jedoch zum Glück noch eine andere Chain zuoberst in die FORWARD
Chain, die DOCKER-USER
Chain. Will man nun effektiv den Zugriff auf Docker Container Services via Host iptables Rules beeinflussen, so muss dies in der DOCKER-USER
Chain mittels manuellen Einträgen gemacht werden. Diese Chain bearbeitet der Docker Daemon nämlich nie und somit werden die manuellen iptables Rules des Systemadministrators dort auch nie überschrieben oder gar umgangen.
Rules added to the FORWARD chain – either manually, or by another iptables-based firewall – are evaluated after these chains. This means that if you expose a port through Docker, this port gets exposed no matter what rules your firewall has configured. If you want those rules to apply even when a port gets exposed through Docker, you must add these rules to the DOCKER-USER chain. –
https://docs.docker.com/network/iptables/#add-iptables-policies-before-dockers-rules.
Nächste Woche folgt Teil 2 dieser Blogpost-Serie.