Default-Deny Stance
A default-deny stance flips the cluster from "everything is allowed" to "nothing is allowed unless you say so." It is the single highest-leverage network control you can apply.
Summary
- Definition: Default-deny means every namespace starts with policies that block all ingress and egress, and you then allow-list only the paths workloads actually need.
- Insight: Because NetworkPolicy is additive, a broad deny plus narrow allows composes cleanly - the deny sets the floor and allows carve exceptions.
- Key Concepts: empty-rule policies, policyTypes, allow-listing, and the DNS exception every egress lockdown needs.
- When to Use: any shared, multi-tenant, or production cluster; roll it out namespace by namespace.
- Limitations: it is only enforced by policy-aware CNIs, and it will break DNS and probes if you allow-list carelessly.
Recipe
Apply a deny-all policy per namespace, then add allows.
- Confirm your CNI enforces NetworkPolicy (Calico, Cilium, or AWS VPC CNI with its policy agent).
- Apply a policy selecting all pods with
policyTypes: [Ingress, Egress]and no rules. - Immediately add a DNS egress allow so name resolution survives.
- Add targeted allow policies for each real dependency.
- Verify with a probe pod that allowed paths pass and everything else drops.
Working Example
Start with the deny-all baseline for a namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {}
policyTypes:
- Ingress
- EgresspodSelector: {} selects every pod, and the absence of ingress/egress blocks both directions.
Now restore DNS, which the deny just cut:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: payments
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53Then allow the one path this app needs - the API tier reaching its database:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-db
namespace: payments
spec:
podSelector:
matchLabels:
app: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 5432Verify from a probe:
kubectl -n payments exec deploy/api -- curl -sS --max-time 3 http://db:5432
kubectl -n payments exec deploy/api -- nslookup dbThe DB call and DNS should succeed; a call to any unrelated pod should hang.
Deep Dive
How additive evaluation works
Policies do not override each other. For a given pod and direction, the effective allow-list is the union of every matching policy's rules.
The deny-all contributes zero allowed sources, so it only matters that some policy selects the pod. Each allow policy then adds permitted flows on top.
Ingress and egress are independent
Selecting a pod for Ingress does not restrict its Egress, and vice versa. You must list a direction in policyTypes for it to be locked.
This is why a pure ingress deny leaves outbound traffic wide open, which is a common half-measure that misses data exfiltration paths.
The DNS trap
An egress deny blocks UDP/TCP 53 to CoreDNS, so every hostname lookup fails. Symptoms look like application errors, not network drops.
Always pair egress deny with a DNS allow, and remember large answers use TCP 53, so allow both protocols.
Rollout order matters
Apply deny-all and its allow-list together, ideally as one GitOps commit. Applying deny-all alone, even briefly, causes an outage window.
In CI you can dry-run with kubectl apply --dry-run=server and validate selectors before merging.
Gotchas
Health probes break. kubelet probes come from the node, not a pod, so pod-to-pod ingress rules will not match them. Allow the node/host path or use CNI features for host traffic.
DNS silently dies. The most frequent post-lockdown incident. Ship the DNS egress allow in the same change as the deny.
Non-enforcing CNI. On the stock AWS VPC CNI without the policy agent, the deny-all is accepted but never enforced, giving false confidence.
Deleting the deny re-opens everything. If the deny-all is removed and no other policy selects a pod, it returns to allow-all instantly.
Namespace labels missing. namespaceSelector matches labels; if kube-system or a peer namespace lacks the expected label, your allow silently matches nothing.
Alternatives
Cluster-wide default-deny (Calico/Cilium). Instead of per-namespace policies, use GlobalNetworkPolicy (Calico) or CiliumClusterwideNetworkPolicy to enforce deny once across the cluster.
Pick this when you own the CNI and want one authoritative baseline rather than N namespace copies.
Admission-driven defaults. Use a controller or policy engine (Kyverno) to auto-inject a deny-all into every new namespace, closing the gap where fresh namespaces start open.
Service mesh mTLS authz. A mesh like Istio enforces identity-based L7 authorization. It complements, but does not replace, L3/L4 NetworkPolicy - use both for defense in depth.
Do nothing (dev only). For an ephemeral single-tenant sandbox, open-by-default may be acceptable. It is not appropriate for anything holding real data.
FAQs
Does default-deny block DNS? Yes, egress deny cuts DNS until you add an allow for UDP/TCP 53 to CoreDNS.
Ingress-only or both directions? Both. Egress control is what contains data exfiltration and lateral outbound scanning.
How do I not break liveness probes? Probes originate from the node; allow host/node traffic or rely on CNI host-endpoint handling rather than pod selectors.
Can I apply this everywhere at once? With Calico or Cilium cluster-wide policies, yes; with core NetworkPolicy you apply per namespace.
What if my CNI ignores policies? The deny is inert. Enable the AWS VPC CNI policy agent or run Calico/Cilium for real enforcement.
How do I audit coverage? List namespaces lacking a default-deny and treat each as a finding; automate it in CI or with a policy engine.
Related
- Why the Cluster Network Is Open by Default
- Network Policies Basics
- DNS Egress Policies
- CNI Comparison
- Network Policies Best Practices
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).