In diesem Teil gibts mehr zu Netfilter & Kubernetes. Die ersten beiden Teile der Blogpost Serie findet ihr hier.
Netfilter & Kubernetes
Nachdem bisher nur Docker/Podman Single Host Setups aufgezeigt wurden, würde ich gerne noch auf den Zusammenhang zwischen netfilter und Kubernetes eingeben. Die ganze Thematik wird nämlich nochmals eine Stufe komplexer, wenn man nun eine Kubernetes Distribution verwenden will (z.B. Rancher mit RKE 1), welche (aktuell zumindest noch) auf den Docker Daemon setzt. Zugleich ist man als Kubernetes Cluster BesitzerIn oftmals weiterhin von iptables auf den Nodes abhängig, da die kube-proxy Komponente für seine Funktionalität massiv auf iptables setzt. Dies zumindest dann, wenn kube-proxy im iptables
Proxy-Mode (--proxy-mode
) verwendet wird – was per Default oftmals der Fall ist (abhängig von der gewählten Kubernetes Distribution). So ist z.B. ein Kubernetes Service (& dessen VIP) in einem 0815 Kubernetes nichts anderes, als ein entsprechender iptables Rule-Eintrag, welcher grob gesagt die Service VIP auf die jeweilige(n) Pod IP Adressen zeigen lässt. Gibt es mehrere Pods (Endpoints) hinter einem Service, so wird der Traffic mittels “unweighted round-robin” verteilt.
iptables ist nun jedoch nicht nur störend wenn im Firmenumfeld sonst alle Linux Systeme bereits via nftables firewalled sind, sondern ab einem gewissen Grad auch immer inperformanter (O(n)
). Eine mögliche Lösung für diese Performanceeinbusse ist der kube-proxy Proxy-Mode IPVS
(da im Hintergrund Hashtables verwendet werden: normalerweise O(1)
, im Worst-Case O(n)
). Bei diesem Mode wird auf das Linux Kernel Modul ip_vs
gesetzt, welches dann natürlich auf den Nodes auch entsprechend installiert und geladen sein muss. Des Weiteren gilt es dabei zu beachten, dass dieser IPVS
Mode mit gewissen anderen Komponenten zum Teil nicht sauber zusammenarbeitet, sodass dann im schlimmsten Fall die eigentlichen Firewall Rules nicht mehr richtig angewendet werden. Dies kommt daher, da Pakete, welche über IPVS
geleitet werden, über andere netfilter Filter Hooks geleitet werden, als dies normalerweise ohne IPVS
der Fall wäre. Mehr Details dazu können im folgenden, sehr empfehlenswerten, Blogpost nachgelesen werden: https://www.digihunch.com/2020/11/ipvs-iptables-and-kube-proxy/
Folgend ein kleines Kubernetes Demosetup, um in einem späteren Schritt die automatisch generierten iptables Rules aufzuzeigen (kube-proxy im Default Proxy-Mode (iptables
)):
Hier das entsprechende YAML Manifest dazu:
--- apiVersion: v1 kind: Namespace metadata: name: testing --- apiVersion: v1 kind: Pod metadata: name: alpine namespace: testing spec: containers: - name: alpine args: - "/bin/sleep" - "3600" image: alpine:3.13 --- apiVersion: apps/v1 kind: Deployment metadata: name: httpd-deployment namespace: testing spec: replicas: 2 selector: matchLabels: app.kubernetes.io/name: httpd template: metadata: labels: app.kubernetes.io/name: httpd spec: containers: - name: httpd image: httpd:2.4.48-alpine ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: httpd-svc namespace: testing labels: service: httpd-svc spec: selector: app.kubernetes.io/name: httpd ports: - protocol: TCP port: 8080 targetPort: 80
Wird das YAML Manifest nun wie folgt auf einem Minikube Kubernetes appliziert, wird es vor allem bei den iptables NAT Table Rules interessant:
# Verifizierung der Minikube Demo Umgebung: root@ubuntu-s-1vcpu-1gb-fra1-01:~# kubectl get nodes 198ms 10:37:42 NAME STATUS ROLES AGE VERSION minikube Ready control-plane,master 54d v1.20.2 # Erstellung der Pods und des Services: root@ubuntu-s-1vcpu-1gb-fra1-01:~# kubectl apply -f iptables_k8s_demo_manifest.yaml 6.9s 10:45:57 namespace/testing created pod/alpine created deployment.apps/httpd-deployment created service/httpd-svc created # Verifizierung des Services: root@ubuntu-s-1vcpu-1gb-fra1-01:~# kubectl get services -n testing 10:37:51 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE httpd-svc ClusterIP 10.107.19.164 <none> 8080/TCP 15s # Verifizierung der Pods: root@ubuntu-s-1vcpu-1gb-fra1-01:~# kubectl get pods -n testing -o wide 112ms 10:38:25 NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES alpine 1/1 Running 0 24s 172.17.0.3 minikube <none> <none> httpd-deployment-59695bd78c-466vs 1/1 Running 0 24s 172.17.0.5 minikube <none> <none> httpd-deployment-59695bd78c-dpfhm 1/1 Running 0 24s 172.17.0.4 minikube <none> <none> # Kommunikation via Service testen: root@ubuntu-s-1vcpu-1gb-fra1-01:~# kubectl exec -it -n testing alpine -- sh 10:41:23 / # nc -vz httpd-svc 8080 httpd-svc (10.107.19.164:8080) open # SSH Session zur Minikube VM erstellen, um die iptables auslesen zu können: root@ubuntu-s-1vcpu-1gb-fra1-01:~# minikube ssh 1.6m 10:43:06 _ _ _ _ ( ) ( ) ___ ___ (_) ___ (_)| |/') _ _ | |_ __ /' _ ` _ `\| |/' _ `\| || , < ( ) ( )| '_`\ /'__`\ | ( ) ( ) || || ( ) || || |\`\ | (_) || |_) )( ___/ (_) (_) (_)(_)(_) (_)(_)(_) (_)`\___/'(_,__/'`\____) $ sudo su # iptables NAT/PAT Rules (auf die relevanten Chains reduzierte Ausgabe): # Anmerkung: Minikube v1.18.1 verwendet im Hintergrund für die Containers die Docker Version 20.10.0. $ iptables -n -v -L -t nat Chain PREROUTING (policy ACCEPT 17 packets, 2100 bytes) pkts bytes target prot opt in out source destination 690 116K KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */ Chain OUTPUT (policy ACCEPT 161 packets, 9660 bytes) pkts bytes target prot opt in out source destination 12843 773K KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */ Chain POSTROUTING (policy ACCEPT 161 packets, 9660 bytes) pkts bytes target prot opt in out source destination 12991 782K KUBE-POSTROUTING all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */ Chain KUBE-MARK-MASQ (11 references) pkts bytes target prot opt in out source destination 2 141 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000 Chain KUBE-POSTROUTING (1 references) pkts bytes target prot opt in out source destination 160 9600 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 mark match ! 0x4000/0x4000 2 141 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK xor 0x4000 2 141 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */ random-fully Chain KUBE-SEP-GFDFIYDM6SQALTPT (1 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ all -- * * 172.17.0.4 0.0.0.0/0 /* testing/httpd-svc */ 1 60 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* testing/httpd-svc */ tcp to:172.17.0.4:80 Chain KUBE-SEP-SNPTLXDNVSPZ5ND2 (1 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ all -- * * 172.17.0.2 0.0.0.0/0 /* kube-system/kube-dns:dns */ 1 81 DNAT udp -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns */ udp to:172.17.0.2:53 Chain KUBE-SEP-TM7P6CHJYRT6MXFR (1 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ all -- * * 172.17.0.5 0.0.0.0/0 /* testing/httpd-svc */ 0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* testing/httpd-svc */ tcp to:172.17.0.5:80 Chain KUBE-SERVICES (2 references) pkts bytes target prot opt in out source destination 1 81 KUBE-MARK-MASQ udp -- * * !10.244.0.0/16 10.96.0.10 /* kube-system/kube-dns:dns cluster IP */ udp dpt:53 1 81 KUBE-SVC-TCOU7JCQXEZGVUNU udp -- * * 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:dns cluster IP */ udp dpt:53 1 60 KUBE-MARK-MASQ tcp -- * * !10.244.0.0/16 10.107.19.164 /* testing/httpd-svc cluster IP */ tcp dpt:8080 1 60 KUBE-SVC-JO5SYOQTVML4RYIV tcp -- * * 0.0.0.0/0 10.107.19.164 /* testing/httpd-svc cluster IP */ tcp dpt:8080 Chain KUBE-SVC-JO5SYOQTVML4RYIV (1 references) # <- 50/50 load-balancing zwischen beiden HTTPD Pods: 172.17.0.4 & 172.17.0.5 pkts bytes target prot opt in out source destination 1 60 KUBE-SEP-GFDFIYDM6SQALTPT all -- * * 0.0.0.0/0 0.0.0.0/0 /* testing/httpd-svc */ statistic mode random probability 0.50000000000 0 0 KUBE-SEP-TM7P6CHJYRT6MXFR all -- * * 0.0.0.0/0 0.0.0.0/0 /* testing/httpd-svc */ Chain KUBE-SVC-TCOU7JCQXEZGVUNU (1 references) pkts bytes target prot opt in out source destination 1 81 KUBE-SEP-SNPTLXDNVSPZ5ND2 all -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns */
Visualisiert sieht die Abhängigkeit zwischen den verschiedenen iptables NAT Chains dann wie folgt aus:
Die KUBE-MARK-MASQ
Chain wird vom kube-proxy dazu verwendet, um für Requests von ausserhalb des Clusters ein Source NAT (SNAT) durchzuführen. Über diese Markierung wird dem Paket schlussendlich in der POSTROUTING
Chain die Source IP Adresse auf die Node IP Adresse gesetzt, sodass es nach erfolgreicher Bearbeitung im Pod seinen Rückweg über den ursprünglichen Eintritts-Node wieder zurück zum Client findet. Für eine genauere Erläuterung dieser Chain und der anderen KUBE-*
Chains sei auf den folgenden Blog verwiesen: https://serenafeng.github.io/2020/03/26/kube-proxy-in-iptables-mode/
Neben dem IPVS
Proxy-Mode, gibt es in der Zwischenzeit noch andere, sehr spannende und viel versprechende Ansätze, welche einem sogar den kompletten Verzicht von iptables im Kubernetes erlauben. Die meiner Meinung nach aktuell vielversprechendsten Ansätze sind die von Cilium mit ihrem “kube-proxy free” Mode und Calico’s eBPF Dataplane. Beide Ansätze setzen auf eBPF als Grundlage und erlauben es deshalb teils massiv besser zu skallieren und zu performen:
- https://cilium.io/blog/2021/05/11/cni-benchmark
- https://www.slideshare.net/ThomasGraf5/cilium-apiaware-networking-and-security-for-containers-based-on-bpf
- https://www.projectcalico.org/introducing-the-calico-ebpf-dataplane/
Mehr über Cilium kann im Blogpost “Cilium on Rancher” nachgelesen werden.
Ausblick
Insgesamt kann meiner Meinung nach gesagt werden, dass es teils sehr erstaunlich ist, wie fest eine Legacy Technologie wie z.B. iptables von 1998 noch so schwerwiegend Einfluss auf neue Technologien wie Docker, Podman und Kubernetes haben kann. Jahrelang war für Kubernetes iptables sogar unverzichtbar. Erst in den Jahren 2019 & 2020 kamen erste vollwertige Alternativen auf den Markt, welche nun aber massiv Anklang zu finden scheinen. Grosse Cloud Provider wie z.B. Google oder Alibaba bieten bereits eBPF basiertes Networking an und es bleibt spannend, wann und in welcher Form die Konkurrenten nachziehen werden. Seitens Single-Host Container Environments scheint sich, zumindest unter Linux, Podman langsam aber sicher gegenüber Docker durchzusetzen. Wenn Podman nun hoffentlich bald auch noch ohne iptables Abhängigkeit daher kommen würde, würde dem modernen Setup mit nftables im Hintergrund nichts mehr im Weg stehen.
Quellenangabe
Weitere Quellen, welche bisher noch nicht verlinkt wurden:
- https://kb.novaordis.com/index.php/Iptables_Concepts
- https://en.wikipedia.org/wiki/Netfilter
- https://developers.redhat.com/blog/2020/08/18/iptables-the-two-variants-and-their-relationship-with-nftables/
- https://workshop.netfilter.org/2019/wiki/images/c/c6/NFWS_2019_-_firewalld%2C_libnftables%2C_and_json%2C_oh_my.pdf