Kubernetes Network Policies: Securing Pod-to-Pod Communication (2026)
By default, every pod in a Kubernetes cluster can communicate with every other pod across all namespaces — an open network model appropriate for development but dangerous in production. Kubernetes Network Policies provide a namespace-scoped firewall mechanism that controls which pods can send and receive traffic. This guide covers the full policy model from deny-all baselines to complex multi-namespace patterns, explains CNI requirements, and provides practical debugging workflows for when policies do not behave as expected.
Table of Contents
How Network Policies Work
Network Policies are additive and whitelist-based. A pod with no NetworkPolicy selecting it is completely open — all ingress and egress is allowed. Once any NetworkPolicy selects a pod, only traffic explicitly permitted by that (or any other matching) policy is allowed. Multiple policies are unioned, not intersected.
The three types of policy targets are:
- Ingress — controls incoming traffic to selected pods
- Egress — controls outgoing traffic from selected pods
- Both — specify both in the
policyTypesarray
CNI Requirements: Calico, Cilium, and Others
The following CNI plugins support Kubernetes NetworkPolicy enforcement:
| CNI Plugin | NetworkPolicy | Extended Policies | eBPF Dataplane |
|---|---|---|---|
| Calico | Yes | GlobalNetworkPolicy, NetworkSet | Optional (eBPF mode) |
| Cilium | Yes | CiliumNetworkPolicy (L7) | Yes (default) |
| Weave Net | Yes | No | No |
| Antrea | Yes | ClusterNetworkPolicy | Optional |
| Flannel | No | No | No |
| AWS VPC CNI | Yes (v1.14+) | No | No |
Cilium's CiliumNetworkPolicy extends standard policies to Layer 7, allowing you to restrict HTTP methods, paths, or gRPC service names — not just IP/port. This is particularly powerful for zero-trust microservice architectures.
Default Deny-All Pattern
The security baseline for any production namespace should be a deny-all policy applied first, then explicit allow policies layered on top. Apply these two policies to every new namespace:
# 1. Deny all ingress in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: payments
spec:
podSelector: {} # Selects ALL pods in the namespace
policyTypes:
- Ingress
# No ingress rules = deny all ingress
---
# 2. Deny all egress in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: payments
spec:
podSelector: {}
policyTypes:
- Egress
# No egress rules = deny all egress
Or combine them into a single policy:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
podSelector and namespaceSelector
Selectors are the core matching mechanism. They use standard Kubernetes label selectors:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: payments
spec:
podSelector:
matchLabels:
app: payments-backend # This policy applies TO these pods
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: payments-frontend # Allow FROM these pods
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: payments # In THIS namespace
ports:
- protocol: TCP
port: 8080
podSelector and namespaceSelector appear in the same from list item (as above), they are ANDed — the source must match both. If you put them as separate items in the list, they are ORed. This is a frequent source of bugs.
# AND (pod in specific namespace):
ingress:
- from:
- podSelector:
matchLabels: {app: frontend}
namespaceSelector:
matchLabels: {env: production}
# OR (pod anywhere, OR any pod in specific namespace):
ingress:
- from:
- podSelector:
matchLabels: {app: frontend}
- namespaceSelector:
matchLabels: {env: production}
Common Production Patterns
Pattern 1: Allow monitoring namespace to scrape all pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-prometheus-scrape
namespace: payments
spec:
podSelector: {} # All pods in payments namespace
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app: prometheus
ports:
- protocol: TCP
port: 9090
- protocol: TCP
port: 8080 # metrics endpoint
Pattern 2: Allow only the API gateway to reach backend services
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-ingress-from-gateway
namespace: payments
spec:
podSelector:
matchLabels:
tier: backend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
Pattern 3: Database tier — only accept connections from app pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: postgres-allow-app
namespace: payments
spec:
podSelector:
matchLabels:
app: postgres
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
needs-db: "true"
ports:
- protocol: TCP
port: 5432
egress: [] # No egress allowed from postgres pods
Egress Rules and External Access
Egress policies are often overlooked but critical for preventing data exfiltration and lateral movement. A common need is to allow DNS resolution and specific external API calls while blocking everything else:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: payments-egress
namespace: payments
spec:
podSelector:
matchLabels:
app: payments-backend
policyTypes:
- Egress
egress:
# Allow DNS (critical — without this, name resolution fails)
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow access to in-cluster services
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: payments
ports:
- protocol: TCP
port: 5432 # PostgreSQL
- protocol: TCP
port: 6379 # Redis
# Allow Stripe API
- to:
- ipBlock:
cidr: 54.187.174.169/32
ports:
- protocol: TCP
port: 443
ipBlock for External CIDRs
The ipBlock field allows or denies traffic to external IP ranges. It supports except for excluding sub-ranges:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-corporate-vpn-ingress
namespace: payments
spec:
podSelector:
matchLabels:
app: admin-panel
policyTypes:
- Ingress
ingress:
- from:
- ipBlock:
cidr: 10.0.0.0/8 # Allow entire RFC1918 private range
except:
- 10.99.0.0/16 # Except the DMZ subnet
ports:
- protocol: TCP
port: 443
ipBlock applies to traffic after NAT. In cloud environments, pod-to-pod traffic within the cluster uses pod CIDRs, while traffic from load balancers may arrive with the node IP. Test your ipBlock rules with actual traffic rather than relying solely on spec reasoning.
Debugging Network Policies
When policies block traffic unexpectedly, use this systematic approach:
# Step 1: Check what policies apply to a pod
kubectl get netpol -n payments -o yaml
# Step 2: Run a debug pod in the same namespace
kubectl run netdebug --image=nicolaka/netshoot -n payments \
--labels="app=payments-frontend" -- sleep 3600
# Step 3: Test connectivity from the debug pod
kubectl exec -n payments netdebug -- curl -v http://payments-backend:8080/health
# Step 4: With Cilium, inspect policy enforcement
kubectl exec -n kube-system cilium-xxxxx -- cilium policy get
kubectl exec -n kube-system cilium-xxxxx -- \
cilium endpoint list | grep "payments"
# Step 5: With Calico, use calicoctl
calicoctl get networkpolicy -n payments -o yaml
# Check effective policy for a specific endpoint
calicoctl get workloadendpoint -n payments -o yaml | grep payments-backend
Cilium provides a policy verdict tool for real-time traffic inspection:
# Monitor policy verdicts (allows and drops)
kubectl exec -n kube-system cilium-xxxxx -- \
cilium monitor --type policy-verdict --from-pod payments/netdebug
Frequently Asked Questions
Do Network Policies apply to host-networked pods?
No. Pods using hostNetwork: true bypass Network Policies entirely — they share the node's network namespace and use node IP addresses. This is one reason to avoid hostNetwork pods except for infrastructure components like CNI agents. Control access to host-network pods at the node firewall level (iptables/nftables) instead.
Why is my policy blocking DNS even though I didn't target DNS?
When you apply an egress deny-all policy, you must explicitly allow UDP/TCP port 53 to the kube-dns service, otherwise all DNS resolution fails and your pods cannot connect to anything by name. Always include a DNS allow rule in any egress policy. Some teams use a namespaceSelector targeting kube-system with a podSelector for k8s-app: kube-dns for a more targeted rule.
Can I write a policy that applies across all namespaces?
Standard Kubernetes NetworkPolicy is namespace-scoped and cannot span namespaces from a single resource. Calico's GlobalNetworkPolicy and Cilium's CiliumClusterwideNetworkPolicy are cluster-scoped CRDs that address this. Use them to enforce baseline security rules (like blocking all pod-to-node-metadata-service traffic) without duplicating policies in every namespace.
How do I allow all pods within a namespace to communicate freely?
Add a policy that allows all ingress from the same namespace: set from to a namespaceSelector matching the current namespace's name label. Since Kubernetes 1.22, all namespaces automatically get the kubernetes.io/metadata.name label equal to the namespace name, which makes this selector reliable.
What happens to existing connections when I apply a new policy?
Network Policy enforcement is connection-level, and the behavior varies by CNI. Calico and Cilium typically enforce new policies on new connection attempts without dropping established TCP connections mid-stream. However, never rely on this — during policy updates, existing flows may be disrupted depending on the CNI version and kernel dataplane mode. Plan policy rollouts during low-traffic windows for critical services.