Lesson 09 of 15 · Traffic Security
Network policies & mTLS
Turn Kubernetes' flat network into a least-privilege one: default-deny policies, allowing exactly the flows you need (including DNS), egress control, CNI-specific extensions, and encrypting service-to-service traffic with mTLS.
Flat by default
Out of the box, every pod can talk to every other pod in the cluster, in every namespace. One compromised pod can probe databases, admin UIs and metadata endpoints. NetworkPolicies turn this into deny by default, allow by intent.
A new school has no walls inside: anyone can wander into any room. NetworkPolicies build walls and add doors with rules: "the kitchen door only opens for the canteen staff", "the library only accepts visitors from classrooms". And don't forget the phone line (DNS). If you wall that off too, nobody can look up where anything is.
How policies behave
- A pod with no policy selecting it: all traffic allowed.
- Once any policy selects a pod for a direction (Ingress or Egress), only traffic explicitly allowed by some policy is permitted in that direction.
- Policies are additive (allow-only), and enforced by the CNI: Calico and Cilium enforce them; some simple CNIs don't.
Step 1: default deny (ingress and egress)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: shop
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"]
Step 2: allow DNS
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 flows
web → api (port 8080), and ingress controller → web:
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-egress-to-api
namespace: shop
spec:
podSelector:
matchLabels: { app: web }
policyTypes: ["Egress"]
egress:
- to:
- podSelector:
matchLabels: { app: api }
ports:
- { protocol: TCP, port: 8080 }
---
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: traefik # the ingress controller's namespace
ports:
- { protocol: TCP, port: 80 }
Selectors in one item vs separate items
Inside a single from item, namespaceSelector and podSelector together mean "these pods in those namespaces". As two separate list items (- namespaceSelector… and - podSelector…), they mean "either". One dash changes the meaning. Review policies carefully.
Beyond the standard API
| Need | Standard NetworkPolicy | CNI extensions |
|---|---|---|
| Cluster-wide defaults | ❌ (per namespace) | Calico GlobalNetworkPolicy, Cilium CiliumClusterwideNetworkPolicy |
Egress by DNS name (api.stripe.com) |
❌ | Cilium FQDN policies, Calico DNS policies |
| L7 rules (HTTP method/path) | ❌ | Cilium L7 policies |
| Policy logging | ❌ | Calico/Cilium flow logs (e.g. Hubble) |
Kubernetes is also standardising cluster-wide AdminNetworkPolicy, supported by some CNIs.
mTLS: encrypt and authenticate workloads
NetworkPolicies filter by labels and IPs. mTLS adds encryption in transit and workload identity:
- Service mesh (Istio, Linkerd): sidecars or node proxies establish mTLS automatically, with identities derived from ServiceAccounts. Policy can say "only
shop/webmay callshop/api", verified cryptographically. - CNI-level encryption (WireGuard/IPsec in Cilium or Calico) encrypts node-to-node traffic transparently, but doesn't give per-workload identity on its own.
- SPIFFE/SPIRE issues workload identities that apps or proxies use for mTLS, across clusters and VMs.
See Service Mesh: Istio & Linkerd for the full treatment.
Try it: isolate the shop (kind + Calico)
- Create a kind cluster with
networking.disableDefaultCNI: trueandpodSubnet: 192.168.0.0/16, then install Calico following its current quickstart manifest. - Deploy
webandapi(any HTTP images, e.g.nginx:1.27on port 80 for both; adjust ports in the policies). - Before policies: confirm web → api and a test pod in
default→ api both work. - Apply default-deny, then check DNS breaks (
nslookup apifails). Add allow-dns and the flow policies. - Verify: web → api works;
default/test→ api times out; api → internet times out.
Going deeper: policy at scale
- Start in audit/observe mode where your CNI supports it (e.g. flow logs) to learn real traffic before denying.
- Ship a default-deny + DNS baseline in every namespace's tenant kit (see Kubernetes Administration, lesson 29), and let teams add allow rules through GitOps.
- Remember host-network pods and the node itself are often outside normal pod policies. Use host endpoint policies (Calico) or host firewall features (Cilium) for nodes.
- Block the cloud metadata endpoint (169.254.169.254) for pods that don't need it. It's a classic credential-theft path.
Recap
- Kubernetes networking is flat by default; the CNI enforces NetworkPolicies.
- Pattern: default deny → allow DNS → allow each real flow (ingress and egress).
- Careful with selector combinations; test with timeouts.
- CNI extensions add global, FQDN and L7 rules; mTLS (mesh, SPIFFE) adds encryption and workload identity.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.