BlogKubernetes

Kubernetes network policies: default-deny, keep DNS up

Lock down pod-to-pod traffic with a default-deny NetworkPolicy, then re-allow DNS and the flows your apps actually need.

Jul 3, 2026·4 min readAdvanced·By the SecOpsLog team · command-tested

Every pod in a default Kubernetes cluster can talk to every other pod, in every namespace. That flat network is convenient during development and exactly what an attacker wants after a single compromised workload. NetworkPolicy is the cluster firewall — but it is inert until your CNI enforces it. Calico, Cilium, and most managed CNIs do; Flannel alone does not. The production pattern is default-deny, then allow: lock the namespace, immediately restore DNS, then open only the flows your architecture actually needs.

The walkthrough below applies a deny-all policy to a shop namespace, re-allows UDP/TCP 53 to CoreDNS, and permits webdb on 5432 — nothing else. If you are still getting comfortable with Services and pod labels, the Kubernetes administration track covers the networking primitives this builds on.

Default-deny with selective allow

Web reaches payments. Reporting and the public internet stop at the policy boundary. DNS egress to CoreDNS stays open.

namespace: prod policy: default-deny ingress: from role=web egress :53 web pod role=web · matched reporting pod role=reporting · no rule public internet 0.0.0.0/0 · external CoreDNS kube-dns · UDP/TCP 53 payments app=payments policy target · deny-all + allow A pod is unrestricted until a policy selects it — then it is deny-all except the explicit allows.
Allowed — web ingress reaches payments Blocked — stopped at the default-deny boundary DNS — egress to CoreDNS on :53
Default-deny rollout in order

Skip DNS and every pod hangs on name resolution. Skip negative testing and you only know the happy path works.

1Confirm CNIenforces NetworkPolicy2Default-denypodSelector: {}3Allow DNSegress UDP/TCP 534Allow app flowslabel-scoped ingress/egress5Negative testunauthorized pod times out6Log droppedCNI flow logs if available7Document matrixwho talks to whom

Confirm your CNI actually enforces policy

Before writing a single manifest, verify enforcement. Apply a deny-all policy to a test namespace and confirm cross-pod traffic stops. If traffic still flows, your CNI is not enforcing — adding more YAML will not help.

bash — flat network before any policylive
kubectl exec -n shop web-0 -- curl -s --max-time 3 db:5432
(connected) — any pod can reach any pod
kubectl get networkpolicy -n shop
No resources found in shop namespace.
convenient for you, and exactly what an attacker wants

Step 1: default-deny the namespace

This policy selects every pod with podSelector: {} and allows no ingress or egress. Apply it and the namespace goes dark — including DNS lookups, which is the trap almost every team hits first.

default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: shop
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
You just broke DNS
Egress deny includes UDP/53 to CoreDNS, so every name lookup fails and apps hang before they ever connect. Re-allow DNS in the very next policy — this trips up almost everyone on their first rollout.

Step 2: allow DNS back

CoreDNS lives in kube-system with the label k8s-app: kube-dns. Allow egress to that pod on both UDP and TCP port 53 — some resolvers fall back to TCP when responses are large.

allow-dns.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: shop
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector: {}
podSelector:
matchLabels: { k8s-app: kube-dns }
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }

Step 3: allow only the flows you need

NetworkPolicy is additive: multiple policies union their allowed rules. Now open the specific path — web pods may reach db on 5432, and nothing else may ingress to db. Remember that policies filter by pod labels, not Service names — the selector must match the pod template labels on your Deployment.

For egress from web to db, you may also need an egress rule on the web pods if default-deny is in effect, because both ingress on the destination and egress on the source must permit the flow. Many teams forget the egress half and wonder why the connection still times out.

web-to-db.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: web-to-db
namespace: shop
spec:
podSelector:
matchLabels: { app: db }
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels: { app: web }
ports:
- { protocol: TCP, port: 5432 }

Verify the denied path, not just the allowed one

A policy that passes the happy-path curl but never tests the negative case is a policy nobody trusts. Spin up a pod without the app: web label and confirm the connection times out.

bash — test both pathslive
kubectl exec -n shop web-0 -- curl -s --max-time 3 db:5432
web -> db:5432 OK
kubectl exec -n shop test-0 -- curl -s --max-time 3 db:5432
timed out — denied, as intended

Where this goes next

NetworkPolicy stops lateral movement inside the cluster. Pair it with RBAC so compromised service accounts cannot read every Secret, and with Pod Security so workloads cannot run as root. The Kubernetes security (CKS) path covers network policy, admission, and runtime guards together.

Go deeper in a courseKubernetes security (CKS-aligned)NetworkPolicy, RBAC, Pod Security and runtime hardening in one track.View course

Related posts