Network policies & default-deny
Segment the cluster without breaking DNS.
The Kubernetes network model is flat and permissive on purpose: every pod gets a routable IP, and by default any pod can open a connection to any other pod on any port, in any namespace, with no NAT in between. That is wonderful for development and a gift to an attacker — a single compromised pod can port-scan every service in the cluster, reach databases it never needed, and exfiltrate data to the internet, all without tripping anything. NetworkPolicy is how you take that back.
A NetworkPolicy is a namespaced, label-selected firewall enforced by your CNI. The critical rule to internalize: a pod is unrestricted until some policy selects it, and the moment one does, that pod switches to deny-all for whichever directions (Ingress, Egress) the policy names — then only the rules you wrote are allowed back in. So segmentation always starts the same way, with a policy that selects every pod and permits nothing.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata:name: default-deny-allnamespace: paymentsspec:podSelector: {} # empty selector = every pod in the namespacepolicyTypes: [Ingress, Egress] # naming a type with no rules = deny that direction
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: { name: allow-dns-egress, namespace: payments }spec:podSelector: {}policyTypes: [Egress]egress:- to:- namespaceSelector:matchLabels: { kubernetes.io/metadata.name: kube-system }ports:- { protocol: UDP, port: 53 }- { protocol: TCP, port: 53 }
Per-consumer allowlists
With deny in place, you reopen exactly the paths that should exist and nothing else. Allow ingress to the payments API only from the web pods that call it, matched by label — never by IP, which changes on every reschedule. Ingress and egress are independent: allowing web-to-payments ingress does not let payments call back out, so a two-way path needs a rule on each side. The payoff is a policy set you can read like a diagram: who talks to whom, on what port, full stop.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: { name: payments-allow-web, namespace: payments }spec:podSelector:matchLabels: { app: payments-api }policyTypes: [Ingress]ingress:- from:- podSelector:matchLabels: { app: web } # same namespace- namespaceSelector:matchLabels: { team: storefront } # or from a whole trusted namespacepodSelector:matchLabels: { app: web }ports:- { protocol: TCP, port: 8080 }
A subtle trap: a from entry that lists both a namespaceSelector and a podSelector as separate list items is an OR (either matches), while the two under a single item is an AND (a pod with that label in a namespace with that label). Getting that wrong is the difference between “web in the storefront namespace” and “anything in storefront, plus any pod labelled web anywhere.”
Prove it both ways
A NetworkPolicy that is never tested is a NetworkPolicy that does not work. Verify from an allowed pod (should connect) and from a disallowed one (should hang and time out, not refuse — deny is a silent drop). If traffic still flows from everywhere, your CNI may not enforce NetworkPolicy at all: Calico, Cilium, and Weave do, and some managed or default “bridge” CNIs silently ignore every policy you write.
# from an allowed pod — connects$ kubectl -n payments exec deploy/web -- curl -sS -m 3 payments-api:8080/healthzok# from a disallowed pod — hangs until the timeout, then fails (silent drop)$ kubectl run probe --rm -it --image=nicolaka/netshoot -n payments -- \curl -m 3 payments-api:8080curl: (28) Connection timed out after 3001 ms