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.
kubectlgeconfigureerd 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
CiliumNetworkPolicyof 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: trueomzeilen 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
- 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.
- AND- versus OR-selectorverwarring. Een enkel
from-item met zowelnamespaceSelectoralspodSelector= AND. Aparte lijstitems = OR. Het verschil is een indentatieniveau in YAML. - 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.
- Ontbrekende namespacelabels. Cross-namespace policies met
namespaceSelectormatchen niets als de doelnamespace niet gelabeld is. Controleer metkubectl get namespace --show-labels. policyTypesweglaten. AlspolicyTypesontbreekt, leidt Kubernetes het af: Ingress wordt altijd aangenomen, Egress alleen als er expliciete egressregels bestaan. Een deny-egress policy zonder egressregels en zonderpolicyTypes: [Egress]doet niets voor egress.hostNetwork-pods negeren. Pods methostNetwork: trueomzeilen NetworkPolicy. Als een gecompromitteerde workload deze capability krijgt, communiceert die vrij.- 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.exceptCIDR-uitsluitingen) - Filteren op Layer 7 (HTTP-methode, pad, headers)
- Egress filteren op DNS-naam
- NodePorts op nodes targeten
- Verkeer naar/van
hostNetwork: truepods beïnvloeden - Bestaande verbindingen beëindigen wanneer policies veranderen