Kubernetes Network Policies: Security Best Practices
Kubernetes networking is permissive by default. Every pod can reach every other pod in the cluster. Every pod can reach the internet. Every pod can be reached from anywhere that can reach the cluster.
This is convenient for development. In production, it’s how a compromised frontend container exfiltrates data through a backend that has database access, or how a misconfigured staging service corrupts production data because nothing stopped it from reaching the production database.
We discovered our staging API could query the production PostgreSQL instance. Same cluster, different namespaces, no network restrictions. Nobody intended this. Kubernetes allowed it by default.
Network policies fix this: firewall rules for pods. Define what’s allowed, deny everything else. They’re the difference between “we trust our code” and “we assume breach.”
Prerequisites: Your CNI Must Support Them
Network policies are Kubernetes resources, but enforcement requires a CNI plugin that supports them: Calico, Cilium, Weave Net, or similar. Flannel’s default config doesn’t enforce network policies.
Verify before investing time:
kubectl get networkpolicies --all-namespaces # Should not error
# Check your CNI docs for NetworkPolicy support
If policies exist but nothing is blocked, your CNI isn’t enforcing them. Ask me how I know.
Default Deny: Start Here
The most important policy: block everything, then allow specifically.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # Applies to ALL pods in namespace
policyTypes:
- Ingress
- Egress
podSelector: {} matches every pod. With no ingress or egress rules, all traffic is denied. Your pods are now isolated—probably too isolated. Add allow policies next.
Apply default deny per namespace, not cluster-wide. Production gets locked down. Development stays permissive (or less locked down).
Allow Specific Traffic: Layer by Layer
Frontend → Backend
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
Only frontend pods can reach backend pods on port 8080. Not staging frontend. Not random debug pods. Not the internet.
Backend → Database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: database-allow-backend
namespace: production
spec:
podSelector:
matchLabels:
app: database
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 5432
Database only accepts connections from backend pods. The compromised frontend from our breach scenario? Can’t reach the database directly.
Cross-Namespace Access
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-monitoring
namespace: production
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: monitoring
podSelector:
matchLabels:
app: prometheus
ports:
- protocol: TCP
port: 9090
Prometheus in the monitoring namespace can scrape metrics. Production API doesn’t accept connections from elsewhere.
This is how we fixed staging → production: explicit namespace selectors ensuring only production backend reaches production database.
Egress Policies: Control Outbound Too
Ingress policies are common. Egress is overlooked—and it’s how data exfiltration happens.
Allow DNS (Required)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Without DNS egress, pods can’t resolve hostnames. Everything breaks mysteriously. Add this before any other egress policy.
Allow External APIs
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-allow-external
namespace: production
spec:
podSelector:
matchLabels:
app: payment-api
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8 # Block internal ranges
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- protocol: TCP
port: 443
Payment API can reach external HTTPS endpoints but not internal cluster IPs. Limits lateral movement if compromised.
Implementation Strategy
Don’t deploy all policies at once. You’ll lock yourself out.
Phase 1: Audit — map current traffic flows (who talks to whom)
Phase 2: Label everything — network policies select by labels; unlabeled pods are invisible to policies
# Every deployment needs consistent labels
metadata:
labels:
app: backend
tier: api
env: production
Phase 3: Default deny in production — one namespace, verify nothing breaks
Phase 4: Add allow policies — one service at a time, test between each
Phase 5: Egress policies — after ingress is stable
Phase 6: Extend to staging/other namespaces
We used Calico’s policy visualization and temporary logging policies to discover traffic we didn’t know about. Expect surprises.
Testing Network Policies
# Test from a debug pod
kubectl run debug --rm -it --image=nicolaka/netshoot -- bash
# Inside the pod:
curl http://backend-service:8080/health # Should work if policy allows
nc -zv database-service 5432 # Should fail if policy denies
nicolaka/netshoot is a Swiss Army knife for network debugging. Keep a debug pod manifest handy.
Common Gotchas
Kubelet health checks: If using network policies on pods with liveness/readiness probes, ensure kubelet can reach pods (usually from host network—not blocked by pod network policies).
Same-namespace traffic: podSelector without namespaceSelector only matches pods in the same namespace.
Policy doesn’t apply to the pod itself: Network policies filter traffic TO/FROM pods—they don’t affect the pod’s own outbound identity.
DNS in kube-system: DNS egress must target kube-system namespace where CoreDNS runs.
CNI not enforcing: Policies exist in API but traffic flows unrestricted. Verify CNI support.
Monitoring and Compliance
- Calico/Cilium dashboards — visualize allowed and denied flows
- Policy audit tools — netpol-analyzer, Cilium Hubble
- GitOps policies — version control network policies like application code
- Regular review — new services need new policies; stale policies accumulate
Conclusion
Network policies implement zero-trust inside your cluster. Default allow is Kubernetes’ out-of-box behavior; default deny is security’s requirement. The gap between them is how staging queries production databases.
Start with default deny in production. Add ingress allows service by service. Don’t forget DNS egress. Label your pods consistently. Test with netshoot before declaring victory.
Our staging-to-production incident wouldn’t have happened with a five-line NetworkPolicy restricting database access to production backend pods. Five lines. That’s the ROI of network segmentation in Kubernetes.
Pods shouldn’t trust each other just because they share a cluster. Network policies make that distrust enforceable.
Kubernetes Network Policies from July 2021, covering security best practices.