Kubernetes NetworkPolicy: default deny and allow only intended traffic
For developers operating workloads in Kubernetes who need to lock down pod-to-pod and ingress traffic without breaking the app. You will apply a namespace-wide default deny policy, add explicit allow rules for DNS, ingress, and app dependencies, and verify the result with concrete commands and expected failures.
TL;DR — Start with a namespace-wide default deny
NetworkPolicy, then add the smallest possible allow rules for DNS, ingress controller traffic, and any app-to-app dependencies. The most common reason policies "do nothing" is that your cluster CNI does not enforceNetworkPolicy; check that first before debugging YAML. Reading time: ~5 min
Goal
When you are done, pods in your target namespace will deny all ingress and egress by default, while only the specific traffic you explicitly allow will work: DNS resolution, traffic from your ingress controller to your app, and any named app-to-app calls you choose to permit.
Prerequisites
kubectlaccess to the target cluster and namespace — check withkubectl version --clientandkubectl auth can-i create networkpolicies -n YOUR_NAMESPACE- A CNI plugin that enforces
NetworkPolicy(for example Calico, Cilium, Antrea, or a managed Kubernetes distribution that documents support) — check withkubectl get pods -n kube-system - A namespace with at least one deployed app and labels on pods you can target — inspect with
kubectl get pods -n YOUR_NAMESPACE --show-labels - A shell with
bash,grep, andcurl; for in-cluster testing, a disposable image such asbusybox:1.36orcurlimages/curl:8.8.0 - The namespace name, the app pod label key/value, and the ingress controller namespace and labels at hand
Steps
Step 1: Confirm your cluster actually enforces NetworkPolicy
Run these commands to identify the CNI and verify you can create policies:
kubectl auth can-i create networkpolicies -n app
kubectl get pods -n kube-system -o wide
kubectl api-resources | grep -i networkpol
Success looks like yes from kubectl auth can-i, and networkpolicies appears in the API resources list.
Typical output shape:
yes
NAME READY STATUS RESTARTS AGE IP NODE
cilium-8k2qj 1/1 Running 0 12d 10.0.1.12 node-a
cilium-operator-6d9d7f6f5f-2wz8p 1/1 Running 0 12d 10.0.1.13 node-a
networkpolicies networking.k8s.io/v1 true NetworkPolicy
Step 2: Inspect labels you will match in policies
List labels on your app pods, your namespace, and the ingress controller namespace:
kubectl get pods -n app --show-labels
kubectl get ns --show-labels
kubectl get pods -A --show-labels | grep -E 'ingress|nginx|traefik|haproxy'
Success looks like a stable label you can target, for example app=api on your workload and kubernetes.io/metadata.name=ingress-nginx on the ingress namespace.
Step 3: Apply a namespace-wide default deny for ingress and egress
⚠️ This can break running applications immediately. If your app needs DNS, database access, or ingress traffic, those flows will stop until you add allow policies in the next steps.
Create the default deny policy:
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: app
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
EOF
Success looks like:
networkpolicy.networking.k8s.io/default-deny-all created
Step 4: Allow DNS egress so pods can resolve names
Most apps need UDP/TCP 53 to CoreDNS. Apply this policy exactly:
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: app
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
EOF
Success looks like:
networkpolicy.networking.k8s.io/allow-dns-egress created
If your cluster labels CoreDNS differently, inspect with kubectl get pods -n kube-system --show-labels | grep -E 'dns|coredns' and replace k8s-app: kube-dns with the actual label.
Step 5: Allow ingress from your ingress controller to your app pods
This example allows traffic from the ingress-nginx namespace to pods labeled app=api on port 8080:
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-ingress-controller
namespace: app
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 8080
EOF
Success looks like:
networkpolicy.networking.k8s.io/allow-from-ingress-controller created
If you use a different controller namespace, replace ingress-nginx with the actual namespace name from kubectl get ns --show-labels.
Step 6: Allow only the app-to-app traffic you intend
Example: allow frontend pods to call backend pods on TCP 8080, and allow backend pods to reach PostgreSQL on TCP 5432.
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: app
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-postgres
namespace: app
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
EOF
Success looks like two created or configured lines.
Step 7: Test from inside the namespace
Start a disposable test pod and check DNS, allowed traffic, and denied traffic:
kubectl run netshoot -n app --rm -it --restart=Never --image=curlimages/curl:8.8.0 -- sh
Inside the shell, run:
nslookup kubernetes.default.svc.cluster.local
curl -sS -o /dev/null -w '%{http_code}\n' http://api:8080/health
curl -m 3 -sS http://postgres:5432
Success looks like DNS resolves, the allowed HTTP endpoint returns 200, and the disallowed or non-HTTP PostgreSQL test times out or fails to speak HTTP. A denied TCP flow commonly looks like:
curl: (28) Connection timed out after 3001 milliseconds
Traffic intent after these policies should look like:
View as text
[Internet] ---- 1. HTTP/HTTPS ----> [Ingress Controller]
|
| 2. TCP 8080
v
[api]
[frontend] ---- 3. TCP 8080 ----> [backend]
[backend] ---- 4. TCP 5432 ----> [postgres]
[all pods] ---- 5. UDP/TCP 53 ----> [CoreDNS]Verify it works
Check the policies exist:
kubectl get networkpolicy -n app
kubectl describe networkpolicy default-deny-all -n app
Expected shape:
NAME POD-SELECTOR AGE
default-deny-all <none> 2m
allow-dns-egress <none> 2m
allow-from-ingress-controller app=api 1m
allow-frontend-to-backend app=backend 1m
allow-backend-to-postgres app=backend 1m
Then verify end-to-end from a test pod:
kubectl run verify -n app --rm -it --restart=Never --image=busybox:1.36 -- sh -c 'nslookup kubernetes.default.svc.cluster.local; wget -S -O- http://api:8080/health || true; nc -vz -w 2 postgres 5432 || true'
Expected result: DNS lookup succeeds, the allowed HTTP endpoint returns HTTP/1.1 200 OK, and any flow you did not explicitly allow fails with timeout or refusal.
Common pitfalls
Your CNI does not enforce NetworkPolicy
Mistake: applying valid NetworkPolicy objects on a cluster whose CNI ignores them.
Symptom: kubectl get networkpolicy -n app shows policies, but all traffic still works unchanged.
Fix: check kubectl get pods -n kube-system; install or enable a CNI that supports NetworkPolicy, then retest.
You forgot DNS egress
Mistake: applying default deny without an allow rule for CoreDNS.
Symptom: apps fail on hostname lookups; logs show errors like no such host, Temporary failure in name resolution, or connection failures to service DNS names.
Fix: apply the allow-dns-egress policy and match the actual CoreDNS pod labels from kubectl get pods -n kube-system --show-labels.
The namespace or pod labels in the policy do not match reality
Mistake: writing selectors from memory instead of from --show-labels output.
Symptom: traffic is denied even though the YAML looks correct.
Fix: run kubectl get pods -n app --show-labels and kubectl get ns --show-labels, then replace selector labels with exact existing key/value pairs.
You allowed ingress to a pod but forgot its egress dependency
Mistake: allowing traffic into backend from frontend, but not allowing backend out to postgres, external APIs, or DNS.
Symptom: the first hop works, but the app still returns 500, 502, or upstream timeout errors.
Fix: add explicit egress policies for each dependency port and destination selector or CIDR.
You used ipBlock for pod IPs
Mistake: allowing pod-to-pod traffic with ipBlock CIDRs based on current pod IPs.
Symptom: it works briefly, then breaks after pod rescheduling or rollout.
Fix: use podSelector and namespaceSelector for Kubernetes-managed workloads; reserve ipBlock for external CIDRs.
You expected NetworkPolicy to filter Service type LoadBalancer or NodePort before kube-proxy/NLB behavior
Mistake: assuming NetworkPolicy replaces cloud firewalls, security groups, or ingress controller config.
Symptom: external reachability does not change the way you expect, or node-level exposure still exists.
Fix: use NetworkPolicy for pod traffic, and separately restrict cloud load balancer rules, node security groups, and ingress resources.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI