Dieser Artikel ist Teil einer Reihe.
- Teil 1: Kubernetes sicher und transparent – Erste Schritte mit Cilium
- Teil 2: Kubernetes sicher und transparent – Erste Schritte mit Cilium (dieser Artikel)
- Teil 3: Kubernetes sicher und transparent – Erste Schritte mit Cilium
Einleitung
Nachdem wir in Teil 1 dieser Reihe unsere lokale Cilium-Umgebung aufgesetzt haben, werden wir in diesem Teil den Cluster mit Leben füllen. Auf geht’s!
Zerstört den Todesstern!
Um Cilium „am lebenden Objekt” zu untersuchen, installieren wir uns die offizielle Cilium Demo. Diese besteht aus drei Services, die wir im default
-Namespace des Clusters installieren:
kubectl create -f https://raw.githubusercontent.com/cilium/cilium/1.17.3/examples/minikube/http-sw-app.yaml
service/deathstar created
deployment.apps/deathstar created
pod/tiefighter created
pod/xwing created
Die Demo beinhaltet ein an Star Wars angelehntes Beispiel, bei dem der Todesstern deathstar
einen HTTP-Webservice bereitstellt. Dieser ist als Service exposed, um via Load Balancing Requests auf die beiden Replikas des Todessterns zu verteilen.
Der Todesstern - für Kulturbanausen: Der gehört zum Empire ;-) - bietet den Tie Fighter-Raumschiffen des Empires einen Raumhafen, auf dem diese einen Stellplatz anfragen können.
Der Tie Fighter tiefighter
als Empire-Raumschiff kann entsprechende Landeanfragen an den HTTP-Endpunkt des Todessterns senden. Der X-Wing xwing
als Allianz-Schiff derzeit allerdings auch.
Aus der Perspektive des Todessterns ist das entsprechend suboptimal, man möchte aus naheliegenden Gründen keine Rebellen auf dem Todesstern. Aber prüfen wir erst einmal, ob das auch wirklich so stimmt.
Für den Tie Fighter:
$ kubectl exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
Ship landed
Und entsprechend für den X-Wing:
$ kubectl exec xwing -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
Ship landed
In der Servicemap auf unserem Hubble UI sehen wir, wie die Topologie zusammenhängt:
Wer die drei Filme kennt, weiß, dass es nicht unbedingt schlau ist, X-Wings in die Nähe von Todessternen kommen zu lassen. Wärmeabzüge - oder auf englisch „thermal exhaust ports” - sorgen für starke Explosionsgefahr. Ihr glaubt mir nicht? Probiert es aus:
kubectl exec xwing -- curl -s -XPUT deathstar.default.svc.cluster.local/v1/exhaust-port
Panic: deathstar exploded
...
Well, that escalated quickly.
Bevor wir uns nun der dunklen Seite der Macht zuwenden, um die Explosion zukünftig durch Ciliums Netzwerkpolicies zu verhindern, schauen wir uns erst wieder die darunter liegenden Techniken und Prinzipien an.
Kubernetes NetworkPolicy vs CiliumNetworkPolicy
Was genau unterscheidet eigentlich die Kubernetes-native NetworkPolicy
von Ciliums eigener Erweiterung, der CiliumNetworkPolicy
? Schauen wir uns das in der Theorie an.
Anatomie einer Kubernetes NetworkPolicy
Um zu verstehen, wie sich die Cilium-Netzwerkpolicies von Kubernetes-nativen Netzwerkpolicies unterscheiden, schauen wir uns zuerst die Kubernetes-Netzwerkpolicies genauer an:
Kubernetes nutzt als Default eine Netzwerkpolicy, die jedwede Pod-zu-Pod-Kommunikation innerhalb des Clusters erlaubt. Das ist - gerade in Zeiten, in denen das Principle of least privilege, Defense in Depth und Zero Trust-Architekturen an Bedeutung gewinnen, nicht wirklich zeitgemäß. Es öffnet [lateral movement](https://en.wikipedia.org/wiki/Lateral_movement_(cybersecurity)-Attacken Tür und Tor. Sind Angreifer im Cluster, haben sie Bewegungsfreiheit, frei nach dem Motto: „Biste drin, mach was du willst”.
Jeder Pod erhält im Kubernetes Networking Model eine eindeutige IP-Adresse und kann mit jedem anderen Pod auf jedem Node des Clusters kommunizieren. Dieser Ansatz wurde (wahrscheinlich) gewählt, weil es dadurch einfach für Ops-Teams war, ihn zu verstehen. Pods verhalten sich so wie normale VMs. Um das Netzwerk aber gegen solche Attacken abzusichern, brauchen wir Netzwerk-Segmentierungsregeln, die den Netzwerkzugriff anhand von Regeln einschränken.
Kubernetes stellt dafür die NetworkPolicy als Mechanismus bereit, mit dem man auf den verschiedenen Cluster-Ebenen (Pod, Service, …) Segmentierungsregeln definieren kann.
Diese Policies operieren auf OSI-Schicht 3 und 4. Das heißt, sie sind in der Lage, anhand von Ports, Adressräumen (CIDRs) und entsprechender Protokolle der Schichten (IP, TCP, UDP, …) Regeln zur Einschränkung des Verkehrs aufzustellen.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-ingress-policy
spec:
podSelector:
matchLabels:
app: database
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 5432
Obige Beispiel-Policy erlaubt es allen Pods mit dem Label app=frontend
, den Port 5432 der Pods mit Label app=database
über das TCP-Protokoll anzusprechen.
Die Durchsetzung der Regeln erfolgt im CNI des Clusters - ja, genau das, das wir in Teil 1 dieser Serie ausgetauscht haben. Es ist also zwingend notwendig, ein CNI zu verwenden, das mit der NetworkPolicy
umgehen kann, was mit dem kind-CNI kindnet
erst seit 2024 möglich ist.
Dazu definiert die NetworkPolicy-Spezifikation vier wichtige Felder:
- podSelector: Definiert, auf welchen Pods die Policy angewandt werden soll
- policyType: Definiert, ob die Policy für eingehenden Traffic (ingress), ausgehenden Traffic (egress) oder beide Arten gelten soll
- ingress und egress: Definieren schließlich die jeweiligen Traffic-Regeln für eingehenden und ausgehenden Traffic.
Die Durchsetzung der NetworkPolicy
in Kubernetes wird vereinfacht gesagt via IP-Adressfiltern umgesetzt. Jeder Pod hat eine eindeutige IP. Entsprechende IP-Adressfilter in Form von IPTables werden vom kube-proxy
verwaltet. Diese Tabellen werden dann auf jedem Node des Clusters vorgehalten und konsistent gehalten.
Das ganze hat leider mindestens einen Haken: Jedes mal, wenn ein Pod gestoppt oder gestartet wird, müssen die IP-Adressfilter auf jedem Node eines Clusters aktualisiert werden. Stellen wir uns mal einen entsprechend großen Cluster vor, mit hunderten oder auch tausenden Nodes. Im Cluster ist „viel los”, neue Pods werden initialisiert, neu gestartet oder herunter skaliert. In diesen Clustern bedeutet das oben beschriebene Verhalten auf schnell eine Aktualisierung von hunderten oder tausenden von Nodes mehrmals in der Sekunde. Dadurch kann es vorkommen, dass z.B. der Start neuer Pods verzögert wird. Er kann erst dann erfolgen, wenn die relevanten Sicherheitsregeln auf den Nodes aktualisiert wurden.
Die Kubernetes-native NetworkPolicy
ist also vereinfacht gesagt der Kubernetes-Mechanismus, um Netzwerk-Segmentierung innerhalb von Clustern umzusetzen. Cilium erweitert die Kubernetes API über Custom Resources und sein eigenes CNI, um die Skalierungsprobleme zu adressieren und mehr Flexibilität und Sicherheit zu ermöglichen.
Cilium entkoppelt Identitäten von IP-Adressen
Bevor wir uns die Ausprägungen von Ciliums Networkpolicies ansehen, schauen wir uns zum Verständnis noch zwei grundlegende Cilium-Konzepte an, den Cilium-Endpoint und die Cilium-Identity.
Cilium nutzt Endpoint und Identity, um die Netzwerk-Kommunikationsverwaltung und deren Sicherheit zu entkoppeln. Eine Workload-Identity besteht dabei über alle Nodes im Cluster.
Die Identity setzt sich aus einer Kombination von User- und System-definierten Labels und Metadaten zusammen. Wird ein Pod erstellt, erstellt Cilium einen entsprechenden Endpoint, der diesen Pod im Netzwerk repräsentiert und weist diesem eine interne IPv4- bzw. IPv6-Adresse zu.
Auch Ciliums Endpoints sind Custom Resources, also Erweiterungen der Kubernetes API. Es ist entsprechend einfach, alle Endpunkte anzeigen zu lassen:
$ kubectl get cep
Führen wir obigen Befehl im Default-Namespace aus, sieht unser Ergebnis wie folgt aus:
Schauen wir uns den tiefighter
-Endpunkt genauer via kubectl describe cep tiefighter
an, sehen wir dort die Attribute der Identität:
...
Identity:
Id: 6528
Labels:
k8s:app.kubernetes.io/name=tiefighter
k8s:class=tiefighter
k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default
k8s:io.cilium.k8s.policy.cluster=default
k8s:io.cilium.k8s.policy.serviceaccount=default
k8s:io.kubernetes.pod.namespace=default
k8s:org=empire
...
Die Endpoint-Identity wird also aus den Labels des Pods und einigen Metadaten-Labels definiert. Ändern sich die Labels, aktualisiert Cilium die Identität des Endpunktes entsprechend. Ändern wir zum Beispiel die org
auf alliance
, ändert sich die interne ID und somit die Identität. Ändern wir das Set an Labels wieder zurück, erhalten sie „eigentlich” die selbe ID wie vorher, sind also effektiv idempotent.
„Eigentlich” ist bekanntermaßen die stärkste Verneinung der deutschen Sprache, also gibt es hier zu beachtende Fallstricke in Form der durch den Cilium-Operator durchgeführten Identity Garbage Collection. Diese löscht Identitäten, wenn sie zu lange nicht aktiv im Cluster vorhanden waren.
Cilium verwaltet Endpunkte also anhand ihrer Identitäten. In unserem lokalen kind-Cluster sind diese CRD-basiert, ihr findet die angelegten Identitäten via kubectl get CiliumIdentity
.
In großen Produktivsystem werden Identitäten wegen der besseren Skalierbarkeit in einem externen Key-Value Store verwaltet.
Netzwerk-Policies in Cilium
Nun, da wir die Konzepte von Endpunkt und Identität kennen, schauen wir uns den Unterschied zwischen Kubernetes-nativen und Cilium-Netzwerkpolicies an. Zuerst aber: Keine Sorge! Cilium ist kompatibel zu den nativen Kubernetes-Policies. Native Policies funktionieren also wie gewohnt weiter, auch wenn ihr Cilium nutzt.
Schauen wir uns die Unterschiede von Kubernetes NetworkPolicy
und CiliumNetworkPolicy
erst einmal aus der Vogelperspektive tabellarisch an:
Merkmal | NetworkPolicy | CiliumNetworkPolicy |
---|---|---|
OSI-Schichten | Layer 3–4 (IP-Adressen, Ports) | Layer 3–7 (inkl. Service-based, HTTP, TLS, DNS) |
Identitätsverwaltung | IP-basiert | Pod-Identitäten (unabhängig von IPs) |
Regelaktualisierungen | iptables, langsame Updates | eBPF Maps (Kernel) |
Flexibilität der Regeln | Basis-Policies | Basis- und erweiterte Policies (Methoden, URLs, Header) |
Steuerungsmodell | Adressen (IP, CIDR), Ports | Labels und Endpoints |
Die CiliumNetworkPolicy ist deutlich mächtiger, da sie nicht nur auf OSI-Schichten 3 und 4 operiert, sondern bis auf OSI-Schicht 7. Dadurch erlaubt sie eine feingranulare Steuerung der Endpunkte anhand ihrer Identitäten (statt IP-Adressen).
Cilium nutzt eine default endpoint policy, die allen ingress- und egress-Traffic für alle Endpunkte im Cluster erlaubt. Wenn ein Endpunkt aber mit irgendeiner Policy-Regel versehen wird, ändert sich dieses Verhalten. Der Endpunkt wechselt in den so genannten default-deny
Zustand, bei dem nur explizit durch die Policy erlaubter Traffic erlaubt wird. Wenn also:
- Irgendeine Regel einen Endpunkt betrifft, und diese Regel eine
ingress
-Sektion beinhaltet, wechselt der Endpunkt automatisch indefault-deny
für ingress traffic. - Irgendeine Regel einen Endpunkt betrifft, und diese Regel eine
egress
-Sektion beinhaltet, wechselt der Endpunkt automatisch indefault-deny
für egress-Traffic.
Zusammenfassend: Endpunkte in Cilium starten per default ohne Restriktionen, aber die erste angewandte Policy ändert dies für die entsprechende Kommunikationsrichtung.
Zwei Beispiele für typische Anwendungsfälle:
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: backend-ingress-policy
spec:
endpointSelector:
matchLabels:
role: backend
ingress:
- fromEndpoints:
- matchLabels:
role: frontend
toPorts:
- ports:
- port: "80"
protocol: TCP
Obige Regeldefinition zeigt eine Endpoint-basierte CiliumNetworkPolicy auf OSI-Schicht 3 und 4, die Traffic nur von Endpunkten mit Label frontend
zu Endpunkten mit Label backend
auf Port 80 erlaubt. Sie wäre mit marginalen Änderungen auch von der Kubernetes-nativen NetworkPolicy abzubilden. Daneben gibt es noch weitere Cilium-Policy-Arten auf diesen Schichten:
Auf OSI-Schicht 3:
- Services-basierte Policies erlauben es, den Traffic von Endpoints zu Services zu steuern.
-
Entities-basierte Policies verwalten Entitäten. Entitäten sind statisch definierte, häufig verwendete Gegenstellen, deren IP-Adresse nicht unbedingt bekannt ist. Sie beinhalten z.B. den
kube-apiserver
,host
,remote-node
oder auchworld
, das identisch zu CIDR 0.0.0.0/0 wäre - also „traffic von außerhalb des Clusters”. - IP/CIDR-basierte Policies definieren Ingress- und Egress-Konnektivitätsregeln für nicht von Cilium verwaltete Endpunkte, die keine Labels besitzen. Typischerweise sind das externe Services, die via statischen IP-Adressen oder Subnetzen referenziert werden.
- DNS-Basierte Policies, die auch für nicht von Cilium verwaltete Endpunkte genutzt werden, die DNS-Domainnames besitzen
- Node-basierte Policies, die Traffic zwischen Nodes steuern
Auf OSI-Schicht 4:
- Port- und Protokollbasierte Policies, analog zu den Kubernetes-nativen Policies.
- ICMP-basierte Policies, die aus- oder eingehende Kommunikation für einzelne ICMP-Typen einschränken, um dadurch z.B. ping flood- oder Ping of Death-Attacken zu verhindern.
Die wirklich interessanten regeln warten aber auf OSI-Schicht 7 auf uns. Interessant deshalb, weil wir hier feingranular Zugriffe auf „die Anwendung”, bzw. deren API steuern können:
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "l7-regel-beispiel"
spec:
endpointSelector:
matchLabels:
app: myService
ingress:
- toPorts:
- ports:
- port: '80'
protocol: TCP
rules:
http:
- method: GET
path: "/documents.*"
- method: PUT
path: "/userinfo$"
headers:
- 'X-Foo-Header: true'
Die obige Beispiel-Policy zeigt eine Schicht-7-Regel, die für Endpunkte mit dem Label app=myService
nur hereinkommende Requests via TCP auf Port 80 zulässt. Okay, ertappt: Das ist Schicht 4, nicht 7.
Diese Regel wird aber auf Schicht 7 unter rules
erweitert:
- Für den Endpunkt
myService
ist festgelegt, dass für das HTTP-Protokoll (OSI-Schicht 7) nurGET
-Requests auf das von einer auf dem Pod laufenden Anwendung bereitgestellte API unter dem Pfad/documents
und alle darunterliegenden Pfade erlaubt sind und… - Sie erlaubt nur
PUT
-Requests unter dem Pfad/userinfo
, ohne trailing slashes oder darunterliegende Pfade. Diese Anfragen sind auch nur erlaubt, sofern ein custom headerX-Foo-Header
den Werttrue
besitzt.
Die path
-Werte sind dabei Reguläre Ausdrücke (POSIX), die mit dem eingehenden Request verglichen werden, was es via "/documents.*"
erlaubt, auch weiterführende Pfade wie etwa /documents/12/details
mit einzuschließen. Alle Pfad-Werte müssen außerdem mit einem /
beginnen.
Es gibt noch weitere Cilium-Policyarten, aber ich denke, die Mächtigkeit und Flexibilität wird hinreichend deutlich.
Die Nutzung der Cilium-Policies erlaubt es, ein konsistentes Netzwerk-Sicherheitsmodell auf verschiedenen Ebenen der Infrastruktur durchzusetzen.
Last but not least gibt es noch den CiliumClusterwideNetworkPolicy
-CRD, der die Kubernetes-API um die Fähigkeit erweitert, clusterweit geltende Policies festzulegen.
apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
name: "clusterwide-policy-example"
spec:
description: "Policy for selective ingress allow to a pod from only a pod with given label"
endpointSelector:
matchLabels:
name: leia
ingress:
- fromEndpoints:
- matchLabels:
name: luke
Obige Policy erlaubt eingehenden Traffic zu Endpunkten mit dem Label name:leia
clusterweit nur von Endpunkten mit dem Label name:luke
. Auch kann man über die CiliumClusterwideNetworkPolicy
sehr einfach „kill switches” implementieren, die allen Traffic zwischen den Pods verbieten, um davon ausgehend den gewünschten Traffic zu whitelisten. Diese Regeln können natürlich weitreichende Auswirkungen haben und sollten entsprechend vorsichtig eingesetzt werden.
Zusammengefasst haben wir durch Ciliums Netzwerkpolicies weit mehr Möglichkeiten, für Sicherheit in unseren Clustern zu sorgen, und einige der Limitierungen der Kubernetes-nativen Implementierung entfallen. So, das war jetzt viel Theorie - was fehlt: Die Anwendung selbiger! Das werden wir in Teil 3 dieser Serie tun.