By default, every pod in a Kubernetes cluster can talk to every other pod, in any namespace. A compromised web pod can therefore scan the database, the metrics endpoints and the cloud metadata service. NetworkPolicies are the firewall rules that close this: allow only the connections an application needs and deny the rest.
Versions in use on this date. Kubernetes 1.22 and Calico 3.20 (August 2021). In 1.22, every namespace automatically carries the label kubernetes.io/metadata.name, which makes namespace selectors reliable. Important: a NetworkPolicy is only an API object; it takes effect only if the cluster’s CNI plugin enforces policies (Calico, Cilium, Weave Net, Antrea). Flannel does not, and the default AWS VPC CNI on EKS does not without adding Calico or another engine; kind’s default CNI does not enforce them either. A policy applied on a non-enforcing cluster silently does nothing.
How selection works
flowchart TB
subgraph NS1[Namespace shop]
WEB[Pods app=web]
API[Pods app=api]
DB[Pods app=db]
end
subgraph NS2[Namespace ingress-nginx]
ING[Ingress controller]
end
subgraph NS3[Namespace monitoring]
PROM[Prometheus]
end
ING -->|allowed 8080| WEB
WEB -->|allowed 8080| API
API -->|allowed 5432| DB
PROM -->|allowed metrics 9100| API
WEB -. denied .-> DB
ING -. denied .-> DB
API -->|allowed UDP and TCP 53| DNS[kube-dns in kube-system]
Rules:
- A policy selects pods with
podSelectorin its own namespace. Pods not selected by any policy stay fully open. - Once any policy selects a pod for a direction (Ingress or Egress), only what some policy allows is permitted in that direction.
- Policies are additive; there are no deny rules and no ordering.
- Allowed peers are identified by
podSelector,namespaceSelector,ipBlock, plus ports. Replies to allowed connections are allowed automatically (stateful).
Step 1: default deny everything in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny-all, namespace: shop }
spec:
podSelector: {} # all pods in the namespace
policyTypes: [Ingress, Egress]
This blocks everything, including DNS, so name lookups stop working. Add DNS next.
Step 2: allow DNS egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-dns, namespace: shop }
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 }
Step 3: allow the real traffic paths
# ingress controller -> web
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: web-from-ingress, namespace: shop }
spec:
podSelector: { matchLabels: { app: web } }
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: ingress-nginx } }
ports: [{ protocol: TCP, port: 8080 }]
---
# web -> api (both directions need rules because both sides are default-denied)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: api-from-web, namespace: shop }
spec:
podSelector: { matchLabels: { app: api } }
policyTypes: [Ingress]
ingress:
- from: [{ podSelector: { matchLabels: { app: web } } }]
ports: [{ protocol: TCP, port: 8080 }]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: web-to-api, namespace: shop }
spec:
podSelector: { matchLabels: { app: web } }
policyTypes: [Egress]
egress:
- to: [{ podSelector: { matchLabels: { app: api } } }]
ports: [{ protocol: TCP, port: 8080 }]
Repeat for api to db (port 5432), Prometheus to metrics ports, and any external dependencies (an ipBlock for an external API; remember to except the cloud metadata address 169.254.169.254/32 when you allow broad ranges).
A subtle point: from entries in separate list items are OR; a single item with both namespaceSelector and podSelector is AND. Getting that indentation wrong either opens too much or blocks everything.
Test it
# should work
kubectl -n shop exec deploy/web -- wget -qO- --timeout=3 http://api:8080/healthz
# should be blocked (times out)
kubectl -n shop exec deploy/web -- nc -zv -w 3 db 5432
# from another namespace
kubectl run -n default tester --rm -it --image=busybox:1.34 --restart=Never -- wget -qO- --timeout=3 http://api.shop:8080
kubectl -n shop get networkpolicy
kubectl -n shop describe networkpolicy api-from-web
Calico adds calicoctl and policy logging for debugging; Cilium offers Hubble flow visibility. Test every policy set in staging, because a mistake produces timeouts that look like application bugs.
Rollout plan
- Confirm the CNI enforces policies (create a deny policy in a test namespace and see a request fail).
- Map real traffic first: service dependency diagrams, Prometheus metrics, or flow logs from the CNI.
- Roll out namespace by namespace: allow policies first, default-deny last.
- Keep policies in Git beside the application manifests, with labels as the contract (
app,tier). - Add egress control gradually: blocking egress to the internet and metadata endpoint limits what an attacker can exfiltrate or fetch.
Limits
NetworkPolicy works at layers 3 and 4 only: no HTTP paths, no DNS-name allow lists (Cilium and Calico offer extensions), and no logging in the core API. A service mesh or Cilium provides identity-based and L7 rules when you need more. NetworkPolicy complements, and does not replace, RBAC, Pod Security and image scanning.