Network Policies Best Practices
This is a practitioner's checklist for adopting NetworkPolicy without outages. Start with namespace isolation, then iterate down to pod-level allow-lists.
How to Use This List
Work top to bottom. The groups are ordered as a rollout path: prove enforcement, establish a default-deny floor, restore essentials, then tighten and operationalize.
Apply changes through GitOps so every policy is reviewed and reversible. Treat each namespace lacking a default-deny as an open finding.
A - Prove Enforcement First
- Confirm the CNI enforces policy. Calico and Cilium enforce natively; the stock AWS VPC CNI needs its network policy agent enabled. A policy on a non-enforcing CNI is silently ignored.
- Test behavior, not existence.
kubectl get netpolproves an object exists, not that it drops traffic. Verify with a probe pod that allowed paths pass and others hang. - Keep a probe workload. Run a small
netshootpod per environment so you can curl and dig from inside the mesh of policies at any time. - Watch for hangs, not refusals. A policy drop usually presents as a timeout, not a connection refused. Learn that signature so you diagnose fast.
B - Establish a Default-Deny Floor
- Deny both ingress and egress. Ingress-only leaves data exfiltration and outbound scanning wide open. List both directions in
policyTypes. - Go namespace by namespace. Roll out per namespace so blast radius is contained if an allow-list is incomplete. Do not flip the whole cluster at once.
- Automate the baseline. Use a controller or Kyverno to inject a default-deny into every new namespace, closing the window where fresh namespaces start open.
- Ship deny and allows together. Apply the deny-all and its allow-list in one commit; a deny alone, even briefly, is an outage.
C - Restore Essentials
- Allow DNS immediately. Egress deny cuts UDP/TCP 53 to CoreDNS, and every hostname lookup then fails. Add the DNS allow in the same change.
- Allow both UDP and TCP 53. Large DNS answers fall back to TCP; omitting it causes rare, hard-to-debug failures.
- Account for health probes. kubelet probes originate from the node, not a pod, so pod-selector ingress rules will not match them. Allow the host/node path or use CNI host-endpoint features.
- Handle NodeLocal DNSCache. If pods query a node-local resolver, a kube-system selector will not match; allow the node-local resolver address via
ipBlock.
D - Tighten to Least Privilege
- Select by label, not by name. Target pods and namespaces via labels so policies survive renames and scale with deployments.
- Mind AND vs OR in
from. Selectors inside one list element are ANDed; separate list items are ORed. Re-read indentation when a rule looks too broad. - Scope ports explicitly. Add the
portslist to each allow so a rule opens only the protocol and port the dependency needs. - Prefer pod-scoped DNS. Once stable, tighten the DNS allow from all of kube-system to the CoreDNS pods via the
k8s-app: kube-dnslabel. - Use CIDR or FQDN egress deliberately. For external dependencies, allow specific CIDRs, or use Cilium
toFQDNsfor per-domain egress; never leave egress open to the internet.
E - Operationalize and Observe
- Store policies as code. Keep every NetworkPolicy in Git under Argo CD or Flux, reviewed like application code, so deletions and edits are auditable.
- Use flow observability. Cilium Hubble shows drop reasons; Calico exposes flow logs. These turn "why is this blocked" from guesswork into evidence.
- Test policies in CI. Validate selectors and run connectivity assertions before merge;
kubectl apply --dry-run=servercatches schema errors early. - Document the intended graph. Maintain a simple allow-list matrix of who-talks-to-whom so reviewers can spot an over-broad rule quickly.
- Re-audit on change. New workloads and namespaces reopen paths; periodically confirm every namespace still has its default-deny and DNS allow.
When You Are Done
Every namespace holding real data has a default-deny for ingress and egress, plus a DNS allow and only the specific allow-rules its workloads need.
Enforcement is verified by probe, policies live in Git, and flow logs let you explain any drop. New namespaces inherit the deny automatically.
FAQs
Where do I start on an existing cluster? Pick one namespace, add a default-deny with DNS and probe allows, verify, then repeat outward.
Namespace isolation or pod-level first? Begin with namespace-level default-deny for a quick safety floor, then iterate to pod-level allow-lists.
How do I avoid breaking DNS? Always ship a DNS egress allow (UDP and TCP 53) in the same change as any egress deny.
How do I know a policy is enforced? Test with a probe pod; a genuine drop shows as a hang. Confirm your CNI enforces policy at all.
Should policies live in Git? Yes. GitOps makes every change reviewed, auditable, and reversible, and prevents silent re-opening.
Do I still need this in a single-tenant cluster? Yes, to contain blast radius if any one workload is compromised.
Related
- Default-Deny Stance
- Network Policies Basics
- DNS Egress Policies
- CNI Comparison
- Why the Cluster Network Is Open by Default
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).