Der blinde Fleck: uneingeschränkter Pod-zu-Pod-Traffic
Ohne explizite NetworkPolicy ist ein Kubernetes-Cluster netzwerkseitig flach. Jeder Pod kann jeden anderen Pod erreichen – unabhängig davon, in welchem Namespace er läuft. Das Frontend im Namespace shop kann direkt mit der Datenbank im Namespace billing sprechen, ein kompromittierter Debug-Pod kann in Sekunden jeden Service im Cluster abklappern, und auch nach außen ist Egress standardmäßig offen. Das ist keine Fehlkonfiguration einzelner Teams, sondern das Standardverhalten der meisten CNI-Implementierungen: Pods bekommen IP-Adressen aus einem gemeinsamen Netzwerk, und ohne eine Regel, die das einschränkt, wird jedes Paket geroutet.
In den letzten Wochen ging es hier um Admission Control mit Kyverno (03.08.), Runtime Security mit Falco (07.08.) und seccomp/AppArmor (29.08.) – also darum, was beim Erstellen eines Pods verhindert und was zur Laufzeit erkannt wird. NetworkPolicies setzen dazwischen an: Sie begrenzen, was ein bereits laufender Pod überhaupt erreichen kann, falls er kompromittiert wird. Kein Ersatz für Admission Control oder Falco, sondern die Segmentierung, die den Schaden eines Kompromisses eindämmt, bevor er sich lateral im Cluster ausbreitet.
Schnell testen lässt sich der Ausgangszustand mit einem Debug-Pod:
kubectl run --rm -it netcheck --image=busybox --restart=Never -- \
wget -qO- --timeout=2 http://backend.other-namespace.svc.cluster.local:8080
Ohne NetworkPolicy antwortet das, egal aus welchem Namespace der Debug-Pod gestartet wird.
NetworkPolicy ist nur ein Vertrag – der CNI muss ihn einhalten
NetworkPolicy ist eine Kubernetes-Standardressource, aber sie ist deklarativ: Das API-Objekt existiert, sobald du es anlegst, unabhängig davon, ob irgendetwas es durchsetzt. Ob die Regel tatsächlich wirkt, entscheidet die CNI-Implementierung. Calico, Cilium und Weave Net implementieren eine Policy-Engine und setzen NetworkPolicies durch. Ein einfaches Flannel-Standardsetup ohne zusätzliche Policy-Engine tut das nicht – die Ressource lässt sich anlegen, wird aber stillschweigend ignoriert. Das ist der häufigste Stolperstein in der Praxis: Policies schreiben, kubectl apply laufen lassen, sich sicher fühlen, ohne dass am Datenpfad irgendetwas passiert ist. Vor der ersten Policy lohnt sich deshalb ein Blick in die CNI-Dokumentation des Clusters, nicht in die Kubernetes-Doku zu NetworkPolicy.
Default-Deny als Ausgangspunkt
Das Grundmuster ist Least Privilege statt Blockliste: erst alles zu, dann gezielt öffnen. Eine Policy mit leerem podSelector und leeren ingress/egress-Listen blockt jeglichen Traffic im Namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: billing
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Ab hier ist der Namespace billing komplett isoliert, ingress wie egress. Alles Weitere ist eine bewusste Ausnahme, keine implizite Erlaubnis.
Gezielt öffnen: nur das Frontend darf zum Backend
Danach erlaubst du exakt die Verbindungen, die gebraucht werden. Zum Beispiel: Nur Pods aus dem Namespace shop mit Label role: frontend dürfen den Backend-Service auf Port 8080 erreichen:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: billing
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: shop
podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 8080
namespaceSelector und podSelector lassen sich kombinieren – die Regel greift nur, wenn beide Bedingungen zutreffen. Für Quellen außerhalb des Clusters gibt es zusätzlich ipBlock, um CIDR-Bereiche gezielt zuzulassen oder auszuschließen.
Der Klassiker: Default-Deny blockt auch DNS
Wer Default-Deny-Egress aktiviert und sich wundert, warum plötzlich gar nichts mehr funktioniert, ist meistens auf diesen Stolperstein gelaufen: DNS-Auflösung läuft über Port 53 zum CoreDNS-Service, und der wird von der Default-Deny-Regel mitblockiert. Ohne DNS kann kein Pod mehr Service-Namen auflösen – auch nicht die, die eigentlich erlaubt wären. Deshalb braucht praktisch jede Egress-Policy diese Ausnahme:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: billing
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Wo die Grenze liegt
NetworkPolicy arbeitet auf Layer 3/4 – IP-Adressen, Namespaces, Ports. Sie kennt keine Workload-Identität, keine Verschlüsselung zwischen Pods und keine Layer-7-Regeln wie „nur GET-Requests auf /api/orders erlauben“. Das ist die Domäne eines Service Mesh wie Istio oder Linkerd, das mit mTLS und identitätsbasierten Policies eine Ebene höher ansetzt. NetworkPolicy ersetzt das nicht – sie ist die günstigere erste Stufe, die mit Bordmitteln des Clusters funktioniert, bevor ein Mesh überhaupt zur Debatte steht.
Schreiben ist nicht prüfen
Eine Policy, die nicht das tut, was du denkst, ist gefährlicher als keine Policy – sie erzeugt ein falsches Sicherheitsgefühl. Nach jeder Änderung lohnt sich ein Test mit einem Debug-Pod aus genau der Position, die die Policy erlauben soll, und aus einer Position, die sie verbieten soll:
kubectl run --rm -it -n shop probe --image=busybox --restart=Never \
-- wget -qO- --timeout=2 http://backend.billing.svc.cluster.local:8080
Für einen schnellen Überblick über bestehende Policies in einem Namespace helfen Werkzeuge wie kubectl-np-viewer (ein Krew-Plugin für kubectl), die die effektiven Regeln visualisieren statt sie aus mehreren YAML-Dateien im Kopf zusammenzusetzen.
Woran es hier weitergeht
Kubernetes-Security ist hier ein wiederkehrendes Thema — von Admission Control über Laufzeit-Erkennung bis zur Netzwerksegmentierung, wie in diesem Artikel. Fragen oder Austausch dazu: mm@mhm-dl.de.
Hinterlasse einen Kommentar