By default, every pod in a Kubernetes cluster can talk to every other pod β regardless of namespace, team, or sensitivity. Network Policies are Kubernetes firewall rules that restrict this. They define which pods are allowed to send traffic to which other pods, and which external IPs can reach your services. Without them, a compromised frontend pod can directly connect to your production database.
+++
Kubernetes Network Policies for Pod-Level Traffic Control
The Default Problem β Every Pod Talks to Every Pod
When you create a cluster without any NetworkPolicy resources, Kubernetes operates in full-open mode. Every pod, in every namespace, can reach every other pod by IP.
Default cluster β no NetworkPolicy: [frontend pod] βββββββββββββββββββ [payments-db pod] β Direct DB access[user-service] βββββββββββββββββββ [payments-db pod] β Should not be allowed[logging-agent] βββββββββββββββββββ [payments-db pod] β Definitely not allowed A compromised frontend can attempt a SQL injection or data dump directly.Network Policies change this to an explicit allowlist model.
How Network Policies Work
A NetworkPolicy is applied to a set of pods (the target) and defines two things:
- Ingress rules: Which pods/IPs are allowed to send traffic to the target
- Egress rules: Which pods/IPs the target is allowed to send traffic to
As soon as a NetworkPolicy selects a pod, that pod moves from "allow all" to "deny all except what the policy explicitly permits."
Before NetworkPolicy:[any pod] β [payments-db] β allowed After applying a NetworkPolicy that selects payments-db:[any pod] β [payments-db] β denied by default[payments-api pod] β [payments-db] β allowed (explicitly permitted in the policy)β οΈ Requirement: NetworkPolicy enforcement requires a CNI plugin that supports it β Calico, Cilium, or Weave. The default CNI on many clusters (Flannel) does not enforce NetworkPolicy. The policies exist in the API but are silently ignored.
The Most Important Policy β Default Deny All
Start with a deny-all policy for your namespace, then add explicit allow rules. This is the production-safe baseline.
1# default-deny-all.yaml2apiVersion: networking.k8s.io/v13kind: NetworkPolicy4metadata:5 name: default-deny-all6 namespace: production7spec:8 podSelector: {} # Empty selector = applies to ALL pods in this namespace9 policyTypes:10 - Ingress11 - Egress12 # No ingress or egress rules = deny everything1kubectl apply -f default-deny-all.yaml2 3# After this, ALL pods in 'production' namespace:4# - Cannot receive any traffic (ingress blocked)5# - Cannot send any traffic (egress blocked)6# Now add explicit allow rules for each serviceAllowing Specific Pod-to-Pod Traffic
1# allow-payments-api-to-db.yaml2apiVersion: networking.k8s.io/v13kind: NetworkPolicy4metadata:5 name: allow-payments-api-to-db6 namespace: production7spec:8 podSelector:9 matchLabels:10 app: payments-db # This policy protects the DB pods11 policyTypes:12 - Ingress13 ingress:14 - from:15 - podSelector:16 matchLabels:17 app: payments-api # Only payments-api pods can connect18 ports:19 - protocol: TCP20 port: 5432 # Only on port 5432 (PostgreSQL)Result: [payments-api pod] β port 5432 β [payments-db pod] β ALLOWED[frontend pod] β port 5432 β [payments-db pod] β DENIED[user-service pod] β port 5432 β [payments-db pod] β DENIED[any other pod] β any port β [payments-db pod] β DENIEDCross-Namespace Traffic
At Razorpay or Zerodha, different teams deploy into different namespaces. An API gateway in the gateway namespace needs to reach services in the production namespace.
1# allow-gateway-namespace.yaml2apiVersion: networking.k8s.io/v13kind: NetworkPolicy4metadata:5 name: allow-from-gateway-ns6 namespace: production7spec:8 podSelector:9 matchLabels:10 app: payments-api11 policyTypes:12 - Ingress13 ingress:14 - from:15 - namespaceSelector:16 matchLabels:17 name: gateway # Allow from any pod in the 'gateway' namespace18 podSelector:19 matchLabels:20 app: nginx-ingress # But only from nginx-ingress pods specifically21 ports:22 - protocol: TCP23 port: 80801# Namespaces need a label for namespaceSelector to work2# Add a label to the gateway namespace first3kubectl label namespace gateway name=gateway4 5# Verify the label6kubectl get namespace gateway --show-labelsEgress Rules β Controlling Outbound Traffic
Egress rules control what a pod can connect to. This is critical for preventing data exfiltration β a compromised pod calling out to an attacker's server.
1# payments-api-egress.yaml2apiVersion: networking.k8s.io/v13kind: NetworkPolicy4metadata:5 name: payments-api-egress6 namespace: production7spec:8 podSelector:9 matchLabels:10 app: payments-api11 policyTypes:12 - Egress13 egress:14 - to:15 - podSelector:16 matchLabels:17 app: payments-db # Allow outbound to DB18 ports:19 - protocol: TCP20 port: 543221 22 - to:23 - podSelector:24 matchLabels:25 app: redis-cache # Allow outbound to Redis26 ports:27 - protocol: TCP28 port: 637929 30 - ports: # Allow DNS resolution (critical β without this,31 - protocol: UDP # the pod cannot resolve any hostname)32 port: 5333 - protocol: TCP34 port: 53β οΈ Always allow DNS (port 53) in egress policies. If you block UDP port 53, the pod cannot resolve any DNS names β including kubernetes.default.svc.cluster.local. This silently breaks all service discovery and HTTP calls even to pods you've explicitly allowed.Allowing External Traffic (from Outside the Cluster)
Traffic from outside the cluster arrives through a LoadBalancer or Ingress controller. The Ingress controller's pod needs to be allowed to reach your application pods.
1# allow-ingress-controller.yaml2apiVersion: networking.k8s.io/v13kind: NetworkPolicy4metadata:5 name: allow-ingress-controller6 namespace: production7spec:8 podSelector:9 matchLabels:10 app: payments-api11 policyTypes:12 - Ingress13 ingress:14 - from:15 - namespaceSelector:16 matchLabels:17 kubernetes.io/metadata.name: ingress-nginx # The ingress controller namespace18 ports:19 - protocol: TCP20 port: 8080Allowing Traffic from Specific IP Ranges
For integrating with external payment gateways or banking APIs that have fixed IP ranges:
1ingress:2 - from:3 - ipBlock:4 cidr: 103.21.244.0/22 # Cloudflare IP range example5 except:6 - 103.21.244.100/32 # Exclude a specific IP within the range7 ports:8 - protocol: TCP9 port: 443Testing That Your Policy Works
Never assume a NetworkPolicy is working. Test it explicitly.
1# Deploy a test pod in the same namespace2kubectl run test-pod \3 --image=busybox \4 --rm -it \5 --restart=Never \6 -n production \7 -- sh8 9# Inside the test pod β try connecting to payments-db10# Replace 10.0.1.50 with the actual pod IP11nc -zv 10.0.1.50 543212# Expected if policy is working: nc: 10.0.1.50 (10.0.1.50:5432): Connection refused13# or it simply hangs and times out14 15# To test allowed traffic, run the test pod with the allowed label16kubectl run test-pod \17 --image=busybox \18 --rm -it \19 --restart=Never \20 --labels="app=payments-api" \21 -n production \22 -- sh23 24nc -zv 10.0.1.50 543225# Expected: Connection to 10.0.1.50 5432 port [tcp/postgresql] succeeded!Complete Production Setup β Layer by Layer
1# Step 1: Apply default deny to the namespace2kubectl apply -f default-deny-all.yaml3 4# Step 2: Allow DNS so pods can resolve service names5kubectl apply -f allow-dns-egress.yaml6 7# Step 3: Allow Ingress controller β app pods8kubectl apply -f allow-ingress-controller.yaml9 10# Step 4: Allow app pods β database11kubectl apply -f allow-payments-api-to-db.yaml12 13# Step 5: Verify all policies are in place14kubectl get networkpolicies -n production15 16# Output:17# NAME POD-SELECTOR AGE18# default-deny-all <none> 5m19# allow-ingress-controller app=payments-api 4m20# allow-payments-api-to-db app=payments-db 3m21# allow-dns-egress app=payments-api 4mDebugging NetworkPolicy Denials
NetworkPolicy denials are silent by default β a blocked connection just times out, no error message says "blocked by NetworkPolicy." This makes debugging hard.
1# Step 1: Check what policies exist and what they select2kubectl get networkpolicies -n production -o wide3 4# Step 2: Describe a specific policy to see its full rules5kubectl describe networkpolicy allow-payments-api-to-db -n production6 7# Step 3: Check pod labels match the policy selectors8kubectl get pod payments-api-7d9f8b-xk2p9 -n production --show-labels9# Verify 'app=payments-api' label is present10 11# Step 4: If using Cilium, use its built-in observability12# kubectl exec -it -n kube-system cilium-xxxxx -- cilium monitor --type drop13# This shows real-time dropped packets with the reasonπ΄ Common Mistake: Applying a NetworkPolicy with a podSelector that does not match any pods due to a label mismatch. The policy is active but protects nothing β all traffic still flows freely. After applying any policy, always run kubectl get networkpolicies -n <namespace> -o wide and verify the POD-SELECTOR column shows the labels you intended.
π‘ Tip: At Hotstar or PhonePe scale, use Cilium instead of Calico if you're on a modern cluster. Cilium's network policies extend beyond port/IP rules to HTTP-layer policies β you can write rules like "allow only GET requests to /api/v1/products from the frontend namespace." This is called Layer 7 policy and it is far more expressive than standard Kubernetes NetworkPolicy.