Originally published on kuryzhev.cloud
Why this checklist
Last audit we ran, the pentest report had one line that made the whole platform team wince: "lateral movement possible between all namespaces." Our cluster had Calico installed, NetworkPolicy support was there, dashboards were green — and yet a compromised pod in a low-trust namespace could reach the billing service directly on port 5432. Nobody had actually written a kubernetes networkpolicy for namespace isolation. The CNI supported it. We just never used it.
Here's the part people miss: without any NetworkPolicy object applied, Kubernetes networking is default-allow. Every pod can reach every other pod, on any port, across every namespace, full stop. That's the platform's factory setting, and it stays that way until you deploy something that says otherwise.
NetworkPolicy is also namespace-scoped and purely additive. You can't "half isolate" a namespace by dropping in a few allow rules — if there's no deny-all baseline, your allow rules add nothing, because everything was already open. This is mistake #1 I see constantly: teams write "allow app-a to talk to app-b" and feel like they've done isolation work. They haven't. Without a deny-all first, that rule is decorative.
What actually forces teams to write this baseline in practice is one of three triggers: a multi-tenant cluster where different teams or customers share nodes, a compliance push (PCI-DSS, SOC2) where an auditor asks "how do you enforce network segmentation between workloads," or exactly what happened to us — a pentest finding lateral movement. If you're reading this before any of those hit you, you're ahead of most teams.
The checklist (numbered)
This is the order we now apply to every namespace before calling it "isolated." Skipping steps, or doing them out of order, is how you end up with policies that look right in kubectl get networkpolicy and do nothing in practice.
-
Default-deny-all ingress. Empty
podSelector: {},policyTypes: [Ingress], no rules. This blocks all inbound traffic to every pod in the namespace unless explicitly allowed. Verify:kubectl describe networkpolicy default-deny-all -n <ns>and confirm ingress is listed. -
Default-deny-all egress. Same pattern with
Egress. This is the one people forget — an empty policy without explicitpolicyTypes: [Egress]only blocks ingress in some Kubernetes versions, egress stays wide open. Explicit is mandatory here, not optional. -
Explicit allow for DNS egress. CoreDNS lives in
kube-systemon 53/UDP and 53/TCP. Skip this and every pod breaks silently right after step 2 lands. Verify:kubectl exec <pod> -- nslookup kubernetes.default. - Explicit allow for intra-namespace pod-to-pod traffic, if your app actually needs it (microservices talking within the same namespace). Don't add this reflexively — plenty of namespaces don't need it.
- Explicit allow for ingress controller / mesh sidecar traffic. Nginx-ingress or Traefik usually sits in its own namespace; without a rule allowing traffic from there, external requests get silently dropped after your baseline lands.
- Allow monitoring scrape traffic. Prometheus pulls metrics, but "pull" still requires an ingress rule on the target side. Miss this and your dashboards go dark with zero error in the app logs.
-
Allow egress to specific external endpoints instead of
0.0.0.0/0. Scope to actual CIDRs — package registries, external APIs — not "port 443 to anywhere," which defeats the entire point of egress isolation. - Verify the CNI actually enforces policy. kube-proxy alone enforces nothing. Confirm with a live traffic test, not by reading YAML.
The reference bundle below covers items 1–4 and 6 for one namespace — apply per-namespace or template it via GitOps.
# baseline-networkpolicy.yaml
# Apply this bundle to EVERY namespace as the isolation baseline.
# kubectl apply -n <namespace> -f baseline-networkpolicy.yaml
---
# 1. Default deny all ingress and egress traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: my-app-ns
spec:
podSelector: {} # applies to all pods in the namespace
policyTypes:
- Ingress
- Egress
# no ingress/egress rules defined = deny everything by default
---
# 2. Allow DNS egress — required after default-deny-egress, or pods can't resolve names
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: my-app-ns
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
# 3. Allow ingress from the shared ingress-controller namespace only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-controller
namespace: my-app-ns
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: my-app # match your ACTUAL pod labels, not assumed ones
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 8080
---
# 4. Allow monitoring namespace to scrape metrics endpoint
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-prometheus-scrape
namespace: my-app-ns
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- protocol: TCP
port: 9090
Commonly missed items
Gotcha #1: DNS breaks after you enable egress deny. The symptom is almost always the same — dial tcp: lookup <service>: i/o timeout, and every app in the namespace starts failing at once. Nine times out of ten it's the missing DNS egress rule. We got bitten by this rolling out the baseline to a staging namespace at 4pm on a Friday — three services went red simultaneously and it took twenty minutes to connect the dots back to the policy we'd just applied.
Namespace labels not being set is the second big one. namespaceSelector rules depend on the automatically-added label kubernetes.io/metadata.name=<ns>, which has existed since Kubernetes 1.21. Older, manually-created namespaces sometimes had this label stripped by an overzealous label-cleanup script or a Helm chart that overwrites labels on upgrade. The policy applies fine, shows up in kubectl get networkpolicy, and matches absolutely nothing. It looks present. It does nothing.
Gotcha #2: hostNetwork pods and DaemonSets bypass NetworkPolicy entirely. kube-proxy, CNI agents, log shippers with hostNetwork: true — none of that traffic is subject to your policies. If your threat model includes "what if someone gets a shell on a node," your NetworkPolicy baseline gives you zero protection there. That's a host-level hardening problem, not a NetworkPolicy problem — don't let the checklist give you false confidence about it.
Mistake #2, which is sneaky: policies written against app: myapp when the Helm chart actually renders app.kubernetes.io/name: myapp. The selector never matches, the policy is a silent no-op, and it survives code review because nobody diffs pod labels against policy selectors. Always run kubectl get pods -n <ns> --show-labels before writing the selector, not after.
Last one: assuming "we have a CNI" means you're covered. Plain Flannel does not enforce NetworkPolicy at all — you need Calico, Cilium, or Weave Net layered on top. Check the official NetworkPolicy docs for the enforcement caveat; it's easy to skim past.
Automation ideas
Writing YAML by hand for every namespace doesn't scale, and it drifts the moment someone runs a quick kubectl edit to unblock a demo. Here's what's worked for us.
Lint in CI before merge. Cilium ships cilium policy validate, and for plain Kubernetes NetworkPolicy YAML, a simple OPA/Conftest rule catches the biggest offender: policies missing Egress from policyTypes. We run this as a required check on every PR touching networkpolicy/.
Bake the baseline into namespace creation. Instead of trusting humans to remember steps 1–3, use an admission webhook — Kyverno or OPA Gatekeeper — that auto-injects the default-deny and DNS-allow policies the moment a namespace is created. We moved to this after the third time someone spun up a namespace via kubectl create ns and forgot the whole checklist existed. If you're managing namespaces via GitOps already, check our DevOps notes on ArgoCD sync patterns for how we template the namespace bootstrap bundle alongside app manifests.
Below is the verification script we run in CI and on-demand after applying the baseline — it's saved us more debugging time than any dashboard.
# Quick verification script — run after applying the baseline
# to confirm enforcement actually works (not just "applied").
NS="my-app-ns"
POD=$(kubectl get pod -n "$NS" -l app.kubernetes.io/name=my-app -o jsonpath='{.items[0].metadata.name}')
echo "== Checking DNS resolution (should succeed) =="
kubectl exec -n "$NS" "$POD" -- nslookup kubernetes.default.svc.cluster.local
echo "== Checking cross-namespace access WITHOUT allow rule (should FAIL/timeout) =="
kubectl exec -n "$NS" "$POD" -- curl -m 2 -sv http://some-service.other-ns.svc.cluster.local:80 \
|| echo "Blocked as expected ✅"
echo "== Checking allowed ingress from ingress-nginx namespace =="
kubectl run tmp-curl --rm -i --tty --restart=Never \
-n ingress-nginx --image=curlimages/curl -- \
curl -m 2 -sv http://my-app.my-app-ns.svc.cluster.local:8080 \
|| echo "Unexpected block ❌ check namespaceSelector labels"
# Sanity check: does the namespace even have the required label?
kubectl get ns my-app-ns --show-labels | grep kubernetes.io/metadata.name \
|| echo "WARNING: namespace missing metadata.name label — namespaceSelector rules won't match"
# Expected output snippet on success:
# Server: 10.96.0.10
# Address: 10.96.0.10:53
# Name: kubernetes.default.svc.cluster.local
# Address: 10.96.0.1
For drift detection, we run a scheduled job that diffs live cluster NetworkPolicies against the git-tracked baseline and alerts on manual edits — anyone editing a policy live via kubectl gets flagged within the hour. And a CI assertion job checks every namespace has both Ingress and Egress in policyTypes, failing the pipeline otherwise. It's a small amount of automation for a large amount of "we didn't actually notice namespace isolation had quietly regressed."
Worth watching: AdminNetworkPolicy (KEP-2091) is moving toward GA and adds cluster-level baseline enforcement that doesn't depend on every namespace remembering its own policy set. Check the Kubernetes docs on AdminNetworkPolicy if you're planning next year's roadmap — it's the direction this checklist is eventually heading.
None of this replaces reviewing your actual kubernetes networkpolicy namespace isolation posture manually at least once a quarter. Automation catches drift; it doesn't catch a threat model that's changed since you wrote the rules.
Top comments (0)