Network Policy Flow
Overview
Traces what happens when a NetworkPolicy object is created — from the API server write through CNI plugin programming to the enforcement of packet-level filtering on nodes.
NetworkPolicy Enforcement Architecture
NetworkPolicy is a Kubernetes API object — it expresses INTENT.
Enforcement is the CNI plugin's responsibility — not kube-proxy.
API Server
│ NetworkPolicy Created
▼
CNI Plugin controller
(Cilium operator / Calico controller / etc.)
│ watches NetworkPolicy objects
▼
Translates to CNI datapath rules
┌─────────────────────────────────────────────┐
│ Cilium: BPF programs loaded per endpoint │
│ Calico: iptables / eBPF rules per node │
│ Flannel: no NetworkPolicy support (!) │
└─────────────────────────────────────────────┘
│
▼
Packet filtering at veth pair
(evaluated per packet at kernel level)
Cilium NetworkPolicy Enforcement Flow
kubectl API Server etcd Cilium Operator Cilium Agent (node) Linux Kernel
│ │ │ │ │ │
│─POST NP───────►│ │ │ │ │
│ (NetworkPolicy) │ │ │ │ │
│ │──WRITE─────►│ │ │ │
│◄─ 201 Created──│ │ │ │ │
│ │ │ │ │ │
│ │──WATCH NP ─────────────► │ │ │
│ │ (NetworkPolicy Added) │ │ │
│ │ │ │ │
│ │ [Cilium Operator │ │
│ │ translates NP to │ │
│ │ CiliumNetworkPolicy │ │
│ │ (CRD)] │ │
│ │◄── CREATE CNP ─────────── │ │ │
│ │──WRITE CNP ──────────────►│(etcd) │ │
│ │ │ │ │
│ │──WATCH CNP ──────────────────────────────────► │
│ │ (CiliumNetworkPolicy Added) │ │
│ │ │ │
│ │ [Cilium agent on each node] │
│ │ fetches endpoint identities │
│ │ compiles BPF program: │
│ │ ┌─────────────────────────┐ │
│ │ │ allow ingress from: │ │
│ │ │ identity=frontend, │ │
│ │ │ port=8080 │ │
│ │ │ deny all other ingress │ │
│ │ └─────────────────────────┘ │
│ │ │ │
│ │ │──bpf_load────────►│
│ │ │ (tc filter │
│ │ │ attached to │
│ │ │ veth pair) │
│ │ │◄── loaded ───────│
│ │ │ │
Policy enforced — all ingress/egress packets evaluated by BPF at kernel level
How Cilium Identifies Pods (Security Identities)
Traditional NetworkPolicy uses IP addresses → breaks with pod restarts.
Cilium uses security identities based on labels:
Pod labels → identity number (e.g., identity=12345 for app=frontend,env=prod)
Identity is embedded in packet metadata (via BPF map lookup)
Flow:
1. Pod created with labels {app:frontend, env:prod}
2. Cilium agent assigns identity=12345 to this label set
3. BPF program on every node stores: identity=12345 → policy allows
4. When frontend pod sends packet to payments-api pod:
- BPF intercepts at veth of sender (egress)
- Looks up sender's identity (12345 = frontend)
- Checks policy map: is 12345 allowed to reach payments-api on port 8080?
- Allow or DROP
iptables-based CNI (Calico without eBPF)
Calico translates NetworkPolicy to iptables chains:
iptables -L | grep cali
KUBE-FORWARD → cali-INPUT → cali-from-hep-... → cali-to-hep-...
cali-tw-payments-api-xxx (traffic to payments-api pod)
→ cali-pi-default.payments-isolation (policy for namespace)
→ match: src=10.0.0.5 (frontend pod IP) → ACCEPT
→ default: DROP
Problem: rules must be updated every time a pod IP changes
(pod restart → new IP → all related iptables rules must be rebuilt)
NetworkPolicy — Default Deny Pattern
# Step 1: Default deny all ingress in namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {} # applies to ALL pods in namespace
policyTypes: [Ingress]
# No ingress rules → deny all
---
# Step 2: Allow only what payments-api needs
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: payments-api-allow
namespace: production
spec:
podSelector:
matchLabels:
app: payments-api
policyTypes: [Ingress, Egress]
ingress:
- from:
- podSelector:
matchLabels:
app: nginx-ingress
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
# DNS egress (required for all pods)
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Verifying Policy Enforcement
# Test connectivity after policy applies
kubectl run test --image=nicolaka/netshoot --rm -it -- \
nc -zv payments-api.production.svc.cluster.local 8080
# Should succeed from frontend pod, fail from unrelated pod
# Cilium: check BPF policy for a specific pod
CILIUM_POD=$(kubectl get pod -n kube-system -l k8s-app=cilium \
--field-selector spec.nodeName=$(kubectl get pod payments-api-xxx \
-n production -o jsonpath='{.spec.nodeName}') \
-o jsonpath='{.items[0].metadata.name}')
kubectl exec -n kube-system $CILIUM_POD -- \
cilium endpoint list | grep payments-api
kubectl exec -n kube-system $CILIUM_POD -- \
cilium policy get
# Hubble — observe dropped packets
hubble observe --namespace production --verdict DROPPED
# Trace policy decision for a specific flow
cilium policy trace \
--src-k8s-pod production/frontend-xxx \
--dst-k8s-pod production/payments-api-xxx \
--dport 8080
NetworkPolicy Propagation Timing
kubectl apply NetworkPolicy
→ API server write: ~10ms
→ Cilium operator translation: ~100ms
→ Cilium agent BPF reload per node: ~50-200ms per node
→ BPF program loaded: policy enforced immediately for new packets
Total propagation to all nodes: typically 500ms–3s for large clusters
Note: in-flight TCP connections are NOT immediately dropped when policy
is tightened (BPF evaluates per new connection, not per packet for
established TCP). Connections established before policy apply continue
until they close. For immediate enforcement of existing connections,
use Cilium's policy enforcement: always mode.
Related
- 05 — Network Operations — Hubble, NetworkPolicy ops
- 03 — Network Policies — policy design reference
- 02 — CNI Plugins — Cilium/Calico CNI architecture