Kubernetes Network Policies: Implementing Zero-Trust Pod Communication
The Default Kubernetes Network Is a Flat Trust Model
Every pod in a Kubernetes cluster can talk to every other pod by default. Your payment API can reach your logging sidecar in a different namespace. A compromised node agent can probe every service endpoint in the cluster. There are no questions asked.
This flat model is convenient for development and genuinely difficult to work with at scale. Zero-trust networking in Kubernetes means inverting that default: nothing is allowed unless explicitly permitted. NetworkPolicies are the mechanism.
Before writing a single YAML file, understand the prerequisite: NetworkPolicies are only enforced by your CNI plugin. The Kubernetes API accepts them regardless. If you're running Flannel, those policies do nothing. You need Calico, Cilium, WeaveNet, or another policy-aware CNI.
# Check your CNI
kubectl get pods -n kube-system | grep -E 'calico|cilium|weave|canal'
# Verify policies are being enforced (create a test policy and check enforcement)
kubectl get networkpolicies --all-namespaces
Step 1: Deny All Traffic by Default
Start with a namespace-level default deny. Apply this to every production namespace:
# default-deny-all.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # Applies to ALL pods in the namespace
policyTypes:
- Ingress
- Egress
An empty podSelector matches every pod in the namespace. Specifying both Ingress and Egress in policyTypes with no rules means both are denied.
Apply it and immediately verify nothing can communicate:
kubectl apply -f default-deny-all.yaml
# Test: try to curl between pods
kubectl exec -it debug-pod -n production -- curl --max-time 3 http://backend-service
# Expected: connection timeout — policy is working
Step 2: Allow DNS Resolution
The first thing that breaks after a default-deny is DNS. Your pods can't resolve service names. You need to explicitly permit UDP port 53 egress to kube-dns:
# allow-dns-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
kubectl apply -f allow-dns-egress.yaml
# Verify DNS works now
kubectl exec -it debug-pod -n production -- nslookup backend-service
Step 3: Define Service-to-Service Policies
Now allow only the traffic your application actually needs. Label your pods with meaningful labels — you'll use these in selectors.
# pod labels to set on your deployments
labels:
app: frontend
tier: web
version: v2
Allow frontend to reach backend on port 8080:
# frontend-to-backend.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
Allow backend to reach the database on port 5432:
# backend-to-db.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-db
namespace: production
spec:
podSelector:
matchLabels:
app: postgres
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 5432
Allow Prometheus to scrape all pods on port 9090:
# allow-prometheus-scrape.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-prometheus-scrape
namespace: production
spec:
podSelector: {} # All pods
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app: prometheus
ports:
- protocol: TCP
port: 9090
Cross-Namespace Policies
NetworkPolicies are namespace-scoped, but you can reference other namespaces. The namespaceSelector + podSelector combination (nested in the same from entry) requires BOTH selectors to match. If you put them in separate list items, it's an OR.
# BOTH namespace AND pod label must match (AND logic)
from:
- namespaceSelector:
matchLabels:
env: production
podSelector:
matchLabels:
app: api-gateway
# Either namespace OR pod label matches (OR logic — usually not what you want)
from:
- namespaceSelector:
matchLabels:
env: production
- podSelector:
matchLabels:
app: api-gateway
This AND vs OR distinction is one of the most common policy mistakes. When in doubt, test explicitly.
Policy Reference Table
| Scenario | Policy Type | Key Selectors |
|---|---|---|
| Allow ingress from same namespace | Ingress | podSelector only |
| Allow ingress from specific namespace | Ingress | namespaceSelector + podSelector |
| Allow all egress to internet | Egress | Empty to block (allows all) |
| Allow specific external IP | Egress | ipBlock.cidr |
| Deny all ingress and egress | Ingress + Egress | Empty podSelector, no rules |
| Allow health check from kubelet | Ingress | ipBlock.cidr = node CIDR |
Allowing External Traffic
For pods that need to reach external services (APIs, S3, package registries):
# allow-external-https.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-external-https
namespace: production
spec:
podSelector:
matchLabels:
needs-internet: "true"
policyTypes:
- Egress
egress:
- ports:
- protocol: TCP
port: 443
- ports:
- protocol: TCP
port: 80
For specific CIDR ranges (e.g., your on-prem or a third-party API):
egress:
- to:
- ipBlock:
cidr: 10.100.0.0/16
ports:
- protocol: TCP
port: 443
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- protocol: TCP
port: 443
Validating Your Policies
Don't assume policies work. Test them:
# Deploy a test pod with network tools
kubectl run nettest --image=nicolaka/netshoot -it --rm -n production -- bash
# From inside nettest, verify these should FAIL (default-deny):
curl --max-time 3 http://some-service-you-shouldnt-reach:8080
# Verify these should SUCCEED:
nslookup kubernetes.default
curl --max-time 3 http://backend-service:8080
With Cilium, you get additional tooling:
# Cilium network policy verdicts in real time
cilium monitor --type drop
# Hubble UI shows traffic flows and policy decisions visually
hubble observe --namespace production --verdict DROPPED
Common Mistakes
Forgetting egress policies: Most guides only show ingress. If you declare policyTypes: [Ingress, Egress], you must also write egress rules or all egress is blocked. A pod that can receive traffic but can't send responses is useless.
Not labeling namespaces: namespaceSelector matches namespace labels, not names. Add labels to namespaces: kubectl label namespace monitoring kubernetes.io/metadata.name=monitoring. The kubernetes.io/metadata.name label is automatically set since Kubernetes 1.21.
Testing in the wrong namespace: A policy in production doesn't affect pods in staging even if the services have the same names.
Zero-trust networking is not a single policy — it's a discipline. Start with default-deny, add policies as you document your service dependencies, and treat any podSelector: {} with Egress to all destinations as a debt that needs to be paid down.
Was this article helpful?
Network & Traffic Engineer
Packets don't lie. I design and troubleshoot the network layer that everything else depends on — Nginx, Envoy, HAProxy, DNS, CDNs, and everything in between. If it touches a socket, it's my problem.
Related Articles
Zero-Trust Networking in Kubernetes with Network Policies
How to implement zero-trust networking in Kubernetes using NetworkPolicies — deny by default, allow by exception, and sleep better at night.
Istio Observability and Authorization: Distributed Tracing, Metrics, and Access Policies
How to use Istio's built-in observability — distributed tracing with Jaeger, Prometheus metrics, Kiali service graph — and enforce zero-trust access control with AuthorizationPolicies.
Encrypting Kubernetes Secrets at Rest: Because Base64 Is Not Encryption
How to configure encryption at rest for Kubernetes secrets using KMS providers, because your secrets in etcd are stored in plaintext by default.
Kubernetes Pod Security Standards: A Complete Guide
Learn everything about Kubernetes Pod Security Standards (PSS) and Pod Security Admission (PSA) — from baseline to restricted profiles with practical examples.
Kubernetes Security Hardening for Production: The Complete Guide
Harden Kubernetes clusters for production with RBAC, network policies, pod security standards, secrets management, and admission controllers.
Kubernetes RBAC: A Practical Guide to Least-Privilege Access Control
Implement least-privilege RBAC in Kubernetes to prevent lateral movement and privilege escalation — with real threat models and pipeline-ready examples.
More in Kubernetes
View all →Kubernetes Vertical Pod Autoscaler: Automating Resource Request Tuning In Production
Let me be direct with you: most Kubernetes clusters I audit are hemorrhaging money because of poorly configured resource requests. I've seen teams running...
Istio Service Mesh: Installation, Traffic Management, and mTLS
A practical guide to getting started with Istio — installing on Kubernetes, enabling automatic mTLS, configuring VirtualServices for traffic management, and understanding the sidecar injection model.
Fix Helm 'UPGRADE FAILED: has no deployed releases'
Fix the Helm 'UPGRADE FAILED: has no deployed releases' error when a previous install failed and left the release in a broken state.
Fix Kubernetes 'Evicted' Pods Filling Up the Node
Clean up Kubernetes evicted pods and fix the underlying disk pressure or resource exhaustion that causes pod evictions.