Containers & netfilter (iptables / nftables) – Teil 3

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:

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:

 

Kommentare sind geschlossen.