Kubernetes Network Policies: pod-naar-pod-verkeer beheersen

Standaard kan elke pod in een Kubernetes-cluster elke andere pod bereiken op elke poort. Met NetworkPolicy beperk je dat verkeer op IP- en poortniveau, zodat een gecompromitteerde frontend-pod niet rechtstreeks bij je database kan. Dit artikel loopt door het aanmaken van een deny-all baseline, het toestaan van specifieke ingress- en egressflows, namespace-isolatie en het testen of je policies daadwerkelijk werken.

Doel

Aan het eind van dit artikel heb je een set NetworkPolicy-resources die een zero-trust netwerkhouding afdwingen binnen een Kubernetes-namespace: standaard al het verkeer geweigerd, met expliciete allow-rules voor de flows die je applicatie nodig heeft.

Vereisten

  • Een Kubernetes-cluster op v1.25 of nieuwer met kubectl-toegang en rechten om NetworkPolicy-objecten aan te maken
  • Een CNI-plugin die NetworkPolicy-enforcement ondersteunt (zie de volgende sectie). Zonder ondersteunende CNI worden NetworkPolicy-objecten opgeslagen in de API, maar hebben ze nul effect op verkeer.
  • Minimaal een applicatie achter een Service. NetworkPolicy selecteert pods op label, maar je gebruikt Services voor connectiviteitstests. Heb je een opfrisser nodig over hoe Services pods ontsluiten? Het gelinkte artikel behandelt alle vier de typen.
  • kubectl geconfigureerd voor het doelcluster

CNI-vereiste

Dit is de vereiste die de meeste mensen overslaan, en het leidt tot het slechtste faalscenario: policies die er correct uitzien maar niets doen.

NetworkPolicy-objecten zijn onderdeel van de Kubernetes API, maar de API-server slaat ze alleen op. Enforcement is de taak van de CNI-plugin. Als je CNI geen NetworkPolicy implementeert, is het aanmaken van deze objecten een stille no-op. Geen foutmelding, geen waarschuwing, geen event.

CNI's die NetworkPolicy afdwingen:

CNI Opmerkingen
Calico Meest gebruikte optie voor network policy. Voegt GlobalNetworkPolicy CRD's toe voor clusterwijde regels.
Cilium eBPF-native. Ondersteunt ook CiliumNetworkPolicy voor L7-filtering (HTTP-pad, DNS-gebaseerd) die verder gaat dan de standaard API.
Antrea Ondersteunt ook de opkomende AdminNetworkPolicy API.
Weave Net Eenvoudigere setup, automatische mesh-encryptie.
Canal Flannel voor routing + Calico voor policy. Voegt NetworkPolicy toe aan Flannel-gebaseerde clusters.

CNI's die NetworkPolicy NIET afdwingen:

  • Flannel: doet alleen inter-node routing. Flannels repository stelt expliciet dat het geen NetworkPolicy implementeert.
  • Kubenet: Kubernetes' ingebouwde lichtgewicht optie. Geen policy-enforcement.

Managed Kubernetes-specifiek:

  • GKE: vereist expliciet het inschakelen van "Network policy" of het gebruik van Dataplane V2 (Cilium-gebaseerd).
  • EKS: de AWS VPC CNI kreeg eind 2023 native NetworkPolicy-ondersteuning. Calico en Cilium zijn gangbare overlay-alternatieven.
  • AKS: gebruikt Azure CNI met Calico of Cilium. De policy-engine moet gekozen worden bij het aanmaken van het cluster.
  • k3s: levert standaard Flannel (geen NetworkPolicy). Overstappen naar Calico of Canal is nodig voor enforcement.

Controleer je CNI voordat je policies schrijft. Ik heb teams dagenlang zien debuggen op "kapotte" policies op clusters met Flannel, waar de policies syntactisch correct waren maar geen enforcement hadden.

Hoe NetworkPolicy werkt

Een pod wordt geisoleerd voor een verkeersrichting (ingress, egress of beide) zodra minimaal een NetworkPolicy die pod selecteert en die richting opneemt in policyTypes. Eenmaal geisoleerd wordt al het verkeer in die richting geweigerd, tenzij een expliciete regel het toestaat.

Policies zijn additief. Meerdere policies die dezelfde pod selecteren stapelen: het toegestane verkeer is de unie van alle regels. Er is geen prioriteit, geen volgorde, geen conflictresolutie. Een permissieve regel in de ene policy kan niet overschreven worden door een restrictieve regel in een andere.

Een paar details die er in de praktijk toe doen:

  • NetworkPolicy werkt op OSI-laag 3/4 (IP-adres + TCP/UDP/SCTP-poort). Het kan geen HTTP-headers, paden of DNS-namen inspecteren. Voor L7-filtering gebruik je Cilium's CiliumNetworkPolicy of een service mesh.
  • Voor een succesvolle verbinding moeten zowel het egressbeleid op de bronpod als het ingressbeleid op de doelpod de verbinding toestaan.
  • Pods met hostNetwork: true omzeilen NetworkPolicy volledig. Ze gebruiken het IP van de node en worden als nodeverkeer behandeld.
  • Bestaande verbindingen worden niet verbroken wanneer een policy wordt toegepast. Alleen nieuwe verbindingen worden geëvalueerd.

Stap 1: pas een default deny-all policy toe

Begin met een zero-trust baseline. Deze policy selecteert elke pod in de namespace (lege podSelector: {}) en declareert zowel Ingress als Egress in policyTypes zonder allow-regels:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}       # selecteert alle pods in deze namespace
  policyTypes:
  - Ingress
  - Egress
  # geen ingress- of egressregels = weiger alles

Toepassen:

kubectl apply -f default-deny-all.yaml

Verwacht resultaat: elke pod in de production-namespace is nu geisoleerd. Geen inkomende verbindingen, geen uitgaande verbindingen.

Dit breekt DNS. Pods kunnen servicenamen niet meer resolven omdat DNS-queries (UDP/TCP poort 53 naar kube-dns in kube-system) geblokkeerd worden door de egress-deny. Dat los je op in de volgende stap.

Omdat NetworkPolicy namespace-scoped is, raakt deze policy alleen de production-namespace. Herhaal hem in elke namespace die je wilt isoleren.

Stap 2: sta DNS-egress toe

Zonder DNS werkt niets. Pas deze policy toe in dezelfde namespace zodat alle pods kube-dns kunnen bereiken:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

De namespaceSelector en podSelector staan in hetzelfde to-item. Dat is AND-logica: alleen pods met label k8s-app: kube-dns in de kube-system-namespace worden getarget. Als het aparte lijstitems waren, zou het OR-logica zijn en veel meer matchen dan bedoeld. Dit AND- versus OR-onderscheid is de meest voorkomende NetworkPolicy-misconfiguratie.

Het label kubernetes.io/metadata.name wordt sinds Kubernetes 1.21 automatisch op elke namespace gezet. Het is onveranderlijk en de standaardmanier om een namespace op naam te targeten.

kubectl apply -f allow-dns-egress.yaml

Controleer of DNS werkt:

kubectl run dns-test --image=nicolaka/netshoot --namespace=production \
  --rm -it --restart=Never -- nslookup kubernetes.default
# Verwacht: Name: kubernetes.default, Address: 10.96.0.1 (je cluster-IP)

Stap 3: sta specifiek ingressverkeer toe

Met de deny-all baseline op z'n plek voeg je expliciete allow-regels toe voor het verkeer dat je applicatie nodig heeft.

Ingress van specifieke pods op label

Deze policy staat alleen pods met label app: frontend in dezelfde namespace toe om app: backend pods te bereiken op poort 8080:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080

Als from alleen een podSelector bevat zonder namespaceSelector, matcht het alleen pods in dezelfde namespace als de policy.

Ingress vanuit een specifieke namespace

Laat monitoringtools in de monitoring-namespace metrics scrapen:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-monitoring-scrape
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
    ports:
    - protocol: TCP
      port: 9090

Ingress van een specifieke pod in een specifieke namespace (AND-logica)

Om alleen Prometheus-pods in de monitoring-namespace toe te staan (niet alle pods daar):

ingress:
- from:
  - namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: monitoring
    podSelector:
      matchLabels:
        app: prometheus
  ports:
  - protocol: TCP
    port: 9090

Beide selectors zitten in hetzelfde from-item (zelfde indentatieniveau). Beide moeten matchen. Vergelijk dit met het OR-patroon, waar het aparte lijstitems zijn:

# OR-logica — staat ELKE pod in monitoring toe OF elke prometheus-pod in ELKE namespace
from:
- namespaceSelector:
    matchLabels:
      kubernetes.io/metadata.name: monitoring
- podSelector:
    matchLabels:
      app: prometheus

Het verschil is een YAML-indentatieniveau. Doe je het verkeerd, dan open je verkeer naar veel meer bronnen dan bedoeld.

Stap 4: configureer egressregels

Egressbeleid bepaalt welk uitgaand verkeer pods mogen versturen.

Egress naar een specifieke interne service

Deze policy laat frontend-pods praten met backend-pods op poort 8080 en verder niets (behalve DNS, gedekt door de policy uit stap 2):

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-egress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: frontend
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: backend
    ports:
    - protocol: TCP
      port: 8080

Egress naar een extern API-endpoint

Wanneer een pod een externe service moet bereiken (een payment gateway, een API-provider), gebruik je ipBlock:

egress:
- to:
  - ipBlock:
      cidr: 203.0.113.0/24    # payment gateway CIDR
  ports:
  - protocol: TCP
    port: 443

Named ports: ports-entries in NetworkPolicy kunnen verwijzen naar een named port uit de containerspec (bijv. port: http die matcht met containerPort: 80, name: http). Dat maakt policies bestendig tegen wijzigingen in poortnummers.

Poortbereiken worden ondersteund sinds Kubernetes 1.25 via het endPort-veld:

ports:
- protocol: TCP
  port: 32000
  endPort: 32768

Stap 5: isoleer namespaces

Het meest voorkomende multi-tenant patroon: pods binnen een namespace mogen vrij communiceren, maar cross-namespace verkeer is niet toegestaan.

Ingress-isolatie

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: namespace-isolation-ingress
  namespace: team-a
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector: {}    # elke pod in DEZELFDE namespace

Een podSelector: {} zonder bijbehorende namespaceSelector matcht alleen pods in dezelfde namespace als de policy.

Egress-isolatie (met DNS)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: namespace-isolation-egress
  namespace: team-a
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector: {}    # alleen intra-namespace
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

Een gedeelde service verkeer laten ontvangen van alle namespaces

Voor gecentraliseerde logging, monitoring of een ingress controller:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-all-namespaces
  namespace: shared-services
spec:
  podSelector:
    matchLabels:
      app: log-aggregator
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector: {}    # alle namespaces
    ports:
    - protocol: TCP
      port: 5044

Namespace-isolatie via NetworkPolicy is een laag van een multi-tenant architectuur. Combineer het met RBAC om namespace privilege-escalatie te voorkomen, en met PodSecurity admission om hostNetwork: true te blokkeren (wat NetworkPolicy volledig omzeilt).

Stap 6: test je policies

Policies die er op papier correct uitzien kunnen stil falen. Test beide richtingen: bevestig dat toegestaan verkeer slaagt en geblokkeerd verkeer wordt geweigerd.

Controleer labels en policies

kubectl get pods -n production --show-labels
kubectl get namespace --show-labels
kubectl get networkpolicies -n production
kubectl describe networkpolicy allow-frontend-to-backend -n production

Test connectiviteit met een debug-pod

De nicolaka/netshoot-image bevat curl, nc, nslookup, dig en andere netwerktools:

# Start een pod met het frontend-label
kubectl run test-frontend \
  --image=nicolaka/netshoot \
  --namespace=production \
  --labels="app=frontend" \
  --rm -it --restart=Never -- /bin/bash

Vanuit de pod:

# Dit moet SLAGEN (frontend → backend op 8080 is toegestaan)
curl -v --connect-timeout 5 http://backend-service:8080/health

# Dit moet een TIMEOUT geven (frontend → database op 5432 is niet toegestaan)
curl -v --max-time 5 telnet://postgres-service:5432
# Verwacht: curl: (28) Connection timed out after 5001 milliseconds

Geblokkeerde verbindingen geven een timeout in plaats van een TCP RST. NetworkPolicy dropt pakketten stil op de netwerklaag.

CNI-specifieke observability

Cilium met Hubble:

# Bekijk gedropte flows in real time
hubble observe --namespace production --verdict DROPPED

# Trace een specifieke verbinding
cilium policy trace \
  --src-k8s-pod production:test-frontend \
  --dst-k8s-pod production:backend-pod \
  --dport 8080 --protocol TCP

Calico:

calicoctl get networkpolicy --all-namespaces
kubectl logs -n calico-system -l k8s-app=calico-node | grep -i denied

Cilium ondersteunt ook een auditmode die policy-violations logt in plaats van verkeer te droppen. Dit is de veiligste manier om policies uit te rollen op bestaande productie-workloads: pas policies toe in auditmode, bekijk Hubble voor AUDIT-verdicts, corrigeer regels, en schakel dan over naar enforcement.

Compleet voorbeeld: drie-lagen applicatie

Alles samen voor een namespace met frontend-, backend- en databaselagen:

# 1. Default deny-all
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
# 2. DNS toestaan voor alle pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
---
# 3. Ingress-nginx mag frontend bereiken op poort 80/443
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-to-frontend
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: frontend
  policyTypes: [Ingress]
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx
    ports:
    - protocol: TCP
      port: 80
    - protocol: TCP
      port: 443
---
# 4. Frontend → backend op poort 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: backend
  policyTypes: [Ingress]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          tier: frontend
    ports:
    - protocol: TCP
      port: 8080
---
# 5. Backend → database op poort 5432
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-backend-to-db
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: database
  policyTypes: [Ingress]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          tier: backend
    ports:
    - protocol: TCP
      port: 5432
---
# 6. Frontend-egress naar backend
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-egress
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: frontend
  policyTypes: [Egress]
  egress:
  - to:
    - podSelector:
        matchLabels:
          tier: backend
    ports:
    - protocol: TCP
      port: 8080
---
# 7. Backend-egress naar database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-egress
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: backend
  policyTypes: [Egress]
  egress:
  - to:
    - podSelector:
        matchLabels:
          tier: database
    ports:
    - protocol: TCP
      port: 5432

Controleer de hele keten:

kubectl get networkpolicies -n production
# Verwacht: 7 policies in de lijst

kubectl describe networkpolicy default-deny-all -n production
# PodSelector: <none> (matcht alles)
# Allowing ingress traffic: <none>
# Allowing egress traffic: <none>

Veelgemaakte fouten

  1. DNS vergeten bij het weigeren van egress. Elke applicatie die servicenamen resolvet stopt stil. Rol de DNS-allowregel altijd gelijktijdig of voor de deny-all policy uit.
  2. AND- versus OR-selectorverwarring. Een enkel from-item met zowel namespaceSelector als podSelector = AND. Aparte lijstitems = OR. Het verschil is een indentatieniveau in YAML.
  3. Policies deployen op een CNI die ze niet ondersteunt. Flannel en kubenet accepteren de objecten zonder foutmelding maar dwingen niets af. Controleer je CNI eerst.
  4. Ontbrekende namespacelabels. Cross-namespace policies met namespaceSelector matchen niets als de doelnamespace niet gelabeld is. Controleer met kubectl get namespace --show-labels.
  5. policyTypes weglaten. Als policyTypes ontbreekt, leidt Kubernetes het af: Ingress wordt altijd aangenomen, Egress alleen als er expliciete egressregels bestaan. Een deny-egress policy zonder egressregels en zonder policyTypes: [Egress] doet niets voor egress.
  6. hostNetwork-pods negeren. Pods met hostNetwork: true omzeilen NetworkPolicy. Als een gecompromitteerde workload deze capability krijgt, communiceert die vrij.
  7. Geblokkeerde paden niet testen. Bevestigen dat toegestaan verkeer werkt is niet genoeg. Verifieer expliciet dat verkeer buiten je allow-regels een timeout geeft.

Bekende beperkingen

De standaard NetworkPolicy API kan op Kubernetes v1.35 niet:

  • Clusterwijde policies toepassen (de opkomende AdminNetworkPolicy API, momenteel v1alpha1/v1beta1, adresseert dit)
  • Negatieve/denyregels uitdrukken (behalve ipBlock.except CIDR-uitsluitingen)
  • Filteren op Layer 7 (HTTP-methode, pad, headers)
  • Egress filteren op DNS-naam
  • NodePorts op nodes targeten
  • Verkeer naar/van hostNetwork: true pods beïnvloeden
  • Bestaande verbindingen beëindigen wanneer policies veranderen

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.