NetworkPolicies
NetworkPolicies
By default, every Pod in a Kubernetes cluster can reach every other Pod — across any namespace — on any port. This is the default-allow model: the network is flat and fully open. For a demo cluster it is fine; for a production cluster running multiple services, teams, or tenants, it is a serious security gap. A compromised frontend pod can freely probe your database. A noisy-neighbor bug in one namespace can hammer the endpoints in another.
NetworkPolicy is the Kubernetes primitive that lets you restrict this traffic. Think of it as a Pod-scoped firewall rule: it selects a set of Pods, and then defines which peers — by pod label or namespace — are allowed to reach them (ingress) or that they are allowed to reach (egress). The network plugin (CNI) enforces the rules in the data plane; kube-apiserver only stores the objects.
From Default-Allow to Default-Deny
The shift from default-allow to default-deny is the most impactful security posture change you can make in a cluster. It mirrors the zero-trust principle: nothing is trusted by default; access must be explicitly granted.
A NetworkPolicy selects Pods via podSelector. The critical insight is this: a policy with an empty podSelector ({}) selects all Pods in the namespace. And a policy with no ingress or egress rules means no traffic is allowed on those directions. Combining both gives you a namespace-wide default-deny:
Apply this policy and every Pod in payments immediately stops receiving and sending traffic — including DNS lookups. Do not apply it to kube-system unless you fully understand the implications.
default-deny-all to a namespace in staging, watch what breaks (check kubectl describe pod and CNI logs), then write allow policies for each legitimate flow. This surfaces undocumented dependencies you did not know existed.
Allowing Specific Traffic
After default-deny is in place, you re-open only what is needed. Three kinds of selectors are available in from / to blocks:
podSelector— matches pods in the same namespace by label.namespaceSelector— matches all pods in namespaces whose labels match.ipBlock— matches a CIDR range (useful for external services or on-prem networks).
Selectors inside the same list item are ANDed; separate list items are ORed. This is a common source of confusion.
A realistic allow policy for an api-server deployment that should only accept traffic from the frontend pod, and only on port 8080:
Notice that egress from api-server is not mentioned — because the default-deny-all policy already covers egress, we need a separate egress policy (e.g., to allow api-server to talk to postgres on port 5432 and to kube-dns on UDP 53).
Namespace Isolation with Cross-Namespace Selectors
Production clusters often run a monitoring namespace (Prometheus) that needs to scrape metrics from application pods in many other namespaces. You want to allow that specific cross-namespace path without opening up everything.
First, label the monitoring namespace:
Then add an ingress rule that combines a namespaceSelector and a podSelector — both in the same from list item so they are ANDed (only Prometheus pods from the monitoring namespace, not any pod from monitoring and not Prometheus pods from any namespace):
Diagram: Namespace Isolation with NetworkPolicy
DNS: The Silent Casualty of Default-Deny
The most common mistake after applying default-deny is forgetting to allow egress to kube-dns (port 53, UDP and TCP). Every DNS lookup from a Pod goes through kube-dns in kube-system. When that is blocked, all service discovery breaks — Pods cannot resolve postgres.payments.svc.cluster.local, and the error message is a generic connection timeout, not a DNS error. Always add this egress policy alongside default-deny:
Debugging NetworkPolicies
When traffic is unexpectedly blocked, the fastest path to root cause is:
- Run
kubectl get networkpolicies -n <namespace> -o yamlto dump all policies and verify selectors match your pod labels exactly (a single typo means no rule applies). - Use
kubectl execinto a debug pod and runcurlornc -zv <target> <port>to confirm what is reachable. - Check your CNI logs. Cilium exposes
cilium-dbg monitor --type dropwhich shows the exact policy verdict on every dropped packet — indispensable for production investigations. - Verify pod labels with
kubectl get pod <name> -o jsonpath='{.metadata.labels}'— these are what the NetworkPolicypodSelectormatches against.
Production Best Practices
- Namespace-per-team isolation: Each team owns a namespace, and each namespace gets
default-deny-allas a baseline. Cross-namespace traffic is explicitly documented in policy YAML and code-reviewed like application code. - Label governance matters: NetworkPolicy is only as reliable as your label discipline. Enforce label standards via admission webhooks (OPA/Gatekeeper) so pods cannot be deployed without the labels policies depend on.
- Use Cilium Network Policy or Calico GlobalNetworkPolicy for cluster-wide defaults that apply across all namespaces — standard NetworkPolicy is namespace-scoped.
- Test in CI: Tools like
netassertork8s-netpol-verifycan run connectivity assertions against a cluster and fail a pipeline if a NetworkPolicy regression lets unexpected traffic through.