Kubernetes Network Policies: Default-Deny and Beyond
By default every pod can talk to every other pod. Network Policies fix that. Here's how to roll out default-deny safely, with AI help reasoning about traffic flows.
- #kubernetes
- #network-policy
- #security
- #networking
- #ai
- #zero-trust
Here’s the thing nobody tells you on day one: a stock Kubernetes cluster is a flat network where every pod can reach every other pod, in every namespace, on every port. Your frontend can talk directly to your database. A compromised marketing-site pod can scan your entire payments namespace. That’s the default, and it’s wide open.
Network Policies are how you close it. They’re a firewall expressed as Kubernetes objects, and rolling them out is one of the highest-leverage security improvements you can make. It’s also one of the easiest to get wrong, so let’s do it carefully.
The mental model: additive allow-lists
Network Policies are allow-lists, and they’re additive. This has two consequences people get backwards:
- A pod with no policy selecting it allows all traffic. Policies only restrict pods they select.
- Once a pod is selected by any policy for a direction, everything not explicitly allowed in that direction is denied. Multiple policies on the same pod union their allows.
So the way you “deny” traffic isn’t a deny rule — it’s selecting a pod with a policy that doesn’t list the traffic you want to block.
Check your CNI first
Network Policies are enforced by your CNI plugin, not by Kubernetes itself. Calico, Cilium, and Antrea enforce them. Flannel does not. If you write policies on a cluster running a CNI that ignores them, you get a false sense of security and zero enforcement. Confirm before you start:
kubectl get pods -n kube-system | grep -E 'calico|cilium|antrea'
If you see none of those, your policies may be decorative.
Step 1: default-deny, one namespace at a time
The foundation is a default-deny policy that selects every pod in a namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {} # selects every pod in the namespace
policyTypes:
- Ingress
- Egress
Empty podSelector plus no ingress/egress rules means “select everything, allow nothing.” Apply this to one non-critical namespace first and watch what breaks. Do not apply default-deny cluster-wide on a Friday.
Step 2: allow DNS, or everything breaks
The instant you deny egress, pods can’t resolve DNS, and everything fails in confusing ways. The first allow rule you add, every time:
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
This single omission causes more “Network Policies broke my cluster” incidents than anything else. Allow DNS first.
Step 3: open the paths you actually need
Now add back the real traffic. Let the API pods reach Postgres:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-to-db
namespace: payments
spec:
podSelector:
matchLabels:
app: postgres
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 5432
Build these incrementally, one connection at a time, watching for connection refused and timeouts after each apply.
Where AI helps: reasoning about flows
The hard part of Network Policies isn’t the YAML — it’s holding the whole traffic graph in your head and knowing what a policy will break. That’s exactly where AI helps. Paste your services and the policies and ask:
“Here are my deployments with their labels and these Network Policies. With a default-deny in place, which pod-to-pod connections are still allowed, and which app dependencies will break? Flag any missing DNS egress.”
The model is good at simulating the allow-list union across multiple policies — the tedious bookkeeping you’d otherwise do on a whiteboard. Keep a few of these Kubernetes networking prompts on hand for rollouts.
Common gotchas
- Cross-namespace traffic requires a
namespaceSelector, and the source namespace needs the label you’re matching on. The auto-appliedkubernetes.io/metadata.namelabel is your friend here. - Egress to external IPs (a managed database, an API) uses
ipBlockCIDRs, not selectors — and you still need DNS egress to resolve the hostname. - Policies don’t filter by hostname, only IP/CIDR/selector. If you need L7 hostname filtering, that’s Cilium policies or a service mesh, not plain Network Policies.
Test enforcement, don’t assume it
After applying, prove the deny works. Exec into a pod that shouldn’t reach the database and try:
kubectl exec -it marketing-pod -- nc -zv postgres.payments 5432
A timeout is success. A connection means your policy isn’t selecting what you think — usually a label mismatch.
Rolling out without an outage
The safe sequence: pick a low-risk namespace, apply default-deny, immediately add DNS egress, then add real flows one at a time while watching app logs. Promote namespace by namespace. Keep the default-deny deletion command ready so you can fail open instantly if something critical breaks.
Before merging policies, I run them through the Code Review tool to catch the classic mistakes — a missing DNS rule, a podSelector that’s too broad, an egress policy with no matching ingress on the destination.
A default-deny cluster turns a single compromised pod from a cluster-wide problem into a contained one. The work is methodical, not clever — deny everything, allow DNS, then open exactly the paths your apps need and nothing else.
AI flow analysis is assistive. Always test enforcement with real connection attempts before trusting a policy.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.