NetworkPolicies and Pod-to-Pod Isolation
Every pod can reach every other by default. The additive allow-only model, a default-deny baseline, and the CNI gotcha that makes policies inert.
The Default Is Wide Open
A fresh Kubernetes cluster is a flat network. Every pod can reach every other pod, in every namespace. Your public-facing frontend can open a connection directly to the database. A compromised sidecar in one namespace can scan every service in the cluster.
This is a deliberate default — it makes things work out of the box — and it's rarely what you want past the first week. NetworkPolicy is how you restrict it.
The Model: Additive and Allow-Only
The logic is easy to get backwards, so state it carefully:
- NetworkPolicies only allow traffic. There is no deny rule.
- A pod is unrestricted until at least one policy selects it for a given direction.
- Once selected for a direction, that pod becomes default-deny for that direction, and only explicitly allowed traffic passes.
- Multiple policies selecting the same pod are additive — the union of what they permit.
Point 3 is the mechanism everything else builds on. You don't write a deny rule; you make a pod subject to policy, and denial becomes the baseline.
Ingress and egress are tracked separately. A policy with only an ingress section restricts incoming traffic and leaves outgoing traffic completely unrestricted. Locking down one direction while assuming both are covered is a common and consequential mistake.
Establishing a Default-Deny Baseline
The idiom is a policy that selects every pod and permits nothing:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # {} selects EVERY pod in the namespace
policyTypes:
- Ingress
- EgressAn empty podSelector matches all pods; listing both policyTypes with no rules means nothing is allowed in either direction. From here you open specific paths.
Apply default-deny egress and you will immediately break DNS, because pods can no longer reach CoreDNS. Every hostname lookup fails, which surfaces as confusing application errors rather than an obvious network block. Always pair it with an explicit DNS allowance:
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53Opening Specific Paths
Let the API reach the database, and nothing else:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-allow-api
namespace: production
spec:
podSelector:
matchLabels:
app: postgres # this policy protects the database pods
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: api # only pods labelled app=api
- podSelector:
matchLabels:
app: worker
ports:
- protocol: TCP
port: 5432Three selector types are available in from / to:
- podSelector: { matchLabels: { app: api } } # pods in THIS namespace
- namespaceSelector: { matchLabels: { env: prod } } # all pods in matching namespaces
- ipBlock: # CIDR, for outside the cluster
cidr: 10.0.0.0/8
except: [10.0.5.0/24]YAML subtlety with real consequences. This is two separate sources — pods in this namespace with app=api, OR any pod in a namespace labelled env=prod:
from:
- podSelector: { matchLabels: { app: api } }
- namespaceSelector: { matchLabels: { env: prod } }This is one source requiring both — pods labelled app=api that are also in a namespace labelled env=prod:
from:
- podSelector: { matchLabels: { app: api } }
namespaceSelector: { matchLabels: { env: prod } }One dash. AND versus OR. The second is almost always what people mean, and the first is what they often write — accidentally permitting every pod in every production namespace.
The Gotcha That Wastes the Most Time
NetworkPolicy is enforced by the CNI plugin, not by Kubernetes itself.
The API server will happily accept, store and display your policy. kubectl get networkpolicy lists it. kubectl describe shows the rules. Everything looks correct.
And if your CNI plugin doesn't implement NetworkPolicy, none of it does anything. Flannel, in its default configuration, is the classic example. Calico, Cilium and most managed-cluster CNIs do enforce.
A NetworkPolicy that exists is not a NetworkPolicy that works. This is a security control whose failure mode is silent and whose absence looks identical to success — so always verify with a real connectivity test, never by confirming the object exists.
Verifying Enforcement
Test from a pod that should be blocked:
# Should FAIL once the policy is enforced
kubectl run probe --rm -it --image=busybox --restart=Never -n production \
-- wget -qO- --timeout=3 http://postgres:5432
# Should SUCCEED — a pod with the allowed label
kubectl run probe --rm -it --image=busybox --restart=Never -n production \
--labels="app=api" -- nc -zv postgres 5432The first command timing out is your proof. If it connects, your policy isn't being enforced regardless of what kubectl get shows.
Check yourself
A team applies a default-deny NetworkPolicy and a rule allowing only the API to reach the database. `kubectl get networkpolicy` lists both. A test pod with no matching labels still connects to the database. What's the most likely cause?
A Practical Adoption Path
Going straight to default-deny across a live cluster breaks things you didn't know were connected. A workable sequence:
- Confirm your CNI enforces policy. Test it before writing anything else.
- Map actual traffic. Service mesh telemetry or CNI flow logs show what really talks to what — reliably more than the architecture diagram does.
- Protect the highest-value target first. An ingress policy on the database, allowing only the services that genuinely need it.
- Add default-deny ingress per namespace, then open the paths your mapping revealed.
- Add default-deny egress last, with DNS allowed from the start. Egress is where surprises live — metrics endpoints, external APIs, webhooks.
- Test each step with a pod that should be blocked.
NetworkPolicy operates on IPs and ports — layer 3 and 4. It cannot express "allow GET but not POST" or "require mutual TLS", because it never sees HTTP. Layer-7 policy is a service mesh concern (Istio, Cilium's L7 policy, Linkerd). Knowing where that boundary sits stops you searching for a NetworkPolicy feature that doesn't exist.
Check yourself
A namespace has one NetworkPolicy selecting pods labelled app=api with an ingress rule allowing traffic from app=frontend. What is true about egress from those API pods?
What's Next
Traffic is routed and restricted. Phase 4 covers workloads that remember — persistent storage, StatefulSets, and the resource requests and limits that decide whether your pod gets scheduled at all or quietly OOM-killed.