Kubernetes Default-Deny Network Policy: Microsegmentation Rollout with Cilium vs Calico (2026)
A fresh Kubernetes cluster is a flat network. Every pod can open a connection to every other pod and to most of the internet, and a single compromised container inherits that reach. Most teams know this and still run it in production, because the one control that fixes it, a kubernetes default deny network policy, has a reputation for causing outages: DNS stops resolving, readiness probes fail, an admission webhook times out, and the change gets rolled back at 2 a.m. and never retried.
The reputation is deserved only when the rollout is naive. The policy object itself is three lines of YAML. The hard part is learning what your workloads actually talk to, proving that your allow rules cover it, and enforcing namespace by namespace with a rollback path. This article gives that path, compares how Cilium and Calico help at each step, and explains the failure modes that bite even careful teams.
What this covers: NetworkPolicy semantics and their traps, default-deny ingress and egress patterns with DNS exceptions, how Cilium and Calico differ in identity, L7, FQDN, audit and staged modes, the status of the newer cluster-scoped policy API, a four-stage rollout workflow, CI tests, and the outages to expect.
Context and Background
Kubernetes NetworkPolicy has been a stable API in networking.k8s.io/v1 for years, yet it is only a specification. The API server stores the object and does nothing else. Enforcement belongs to the network plugin, and the upstream documentation is blunt about it: creating a NetworkPolicy without a controller that implements it has no effect. A cluster on a plugin that ignores policy will accept your YAML, report success, and enforce nothing. This is the first thing to verify in any audit, and the cheapest test is to apply a default-deny in a scratch namespace and confirm that a curl between two pods actually fails.
The semantics are deliberately narrow. NetworkPolicy operates at layer 3 and 4: IP addresses, ports, and the TCP, UDP and SCTP protocols. It selects pods by label, namespaces by label, and external peers by CIDR through ipBlock. It cannot match an HTTP path, a DNS name, a service account, or a TLS identity. It has no deny rule, no logging, no priority, and no way to target a node. Everything richer than that is a CNI extension, which is why the choice between Cilium and Calico matters. For a broader view of how the plugins differ in datapath and operations, see our CNI comparison of Calico, Cilium, Flannel and Multus, and for an industrial-edge decision record see the Cilium vs Calico ADR for industrial Kubernetes networking.
The two projects approach extension differently. Cilium identifies workloads by a numeric security identity derived from labels, enforces in eBPF programs, and adds CiliumNetworkPolicy and CiliumClusterwideNetworkPolicy with L7 rules and DNS-aware egress. Calico keeps the Kubernetes model, adds NetworkPolicy and GlobalNetworkPolicy in the projectcalico.org/v3 API, and layers an ordering system, tiers, on top. Both support the upstream NetworkPolicy object, so a default-deny written in plain Kubernetes YAML is portable. That portability is a feature worth protecting: put your baseline in upstream resources and reserve vendor resources for what upstream cannot express.
Third, there is the cluster-scoped API from SIG Network’s Network Policy API subproject. Its first generation, AdminNetworkPolicy and BaselineAdminNetworkPolicy, shipped as v1alpha1 custom resources in the policy.networking.k8s.io group. The subproject has since announced that a v1alpha2 resource, ClusterNetworkPolicy, replaces both. We return to it below because it changes who is allowed to own the deny.
The motivating threat model is lateral movement. Attackers who land in one pod through a vulnerable dependency or a leaked token scan the pod network, reach internal databases and metadata endpoints, and pivot. Default deny does not stop the initial compromise, but it collapses the blast radius from “the cluster” to “what this workload was explicitly allowed to reach.” That is the practical content of zero trust kubernetes networking at the packet layer: no implicit trust from network location, with every flow justified by a rule someone reviewed.
Reference Architecture: How Default Deny Actually Works
A default-deny network policy is an ordinary NetworkPolicy whose podSelector matches every pod in a namespace and which lists no allow rules for the chosen direction. Once any policy selects a pod for ingress or egress, that pod becomes isolated in that direction, and only traffic matched by the union of all selecting policies is permitted.

Figure 1: Decision flow for a pod under Kubernetes NetworkPolicy. Selection flips a pod from non-isolated to isolated per direction; allow rules from every selecting policy are unioned.
Figure 1 shows the whole model. Evaluation is per pod and per direction. If no policy selects the pod for ingress, ingress is open. The moment one does, ingress is closed except for what the union of rules from all selecting policies permits. Egress is evaluated independently, which is the source of the most common half-finished rollout: a team writes default-deny for ingress, calls it done, and leaves every pod free to exfiltrate over egress.
Additive semantics and why there is no “deny”
NetworkPolicy rules only ever allow. Ten policies selecting the same pod do not conflict; they merge, and the pod can do anything any one of them permits. The consequence for operations is that you cannot fix an over-broad allow by adding a deny elsewhere. You must find and narrow the allow. The consequence for rollouts is more welcome: adding a policy can only widen what is permitted once isolation has been triggered, so allow rules are safe to add incrementally, and the dangerous act is the first selection that triggers isolation.
Return traffic is handled statefully. If a pod is allowed to initiate a connection to a database on 5432, the replies flow back without an ingress rule on the client. You only describe the direction in which the connection is opened. This is the reason a correct policy set is roughly half the size people expect, and the reason debugging should always begin by asking who initiated the flow.
Selectors: the one-dash bug
A peer in from or to can be a podSelector, a namespaceSelector, a combination, or an ipBlock. The combination has a famous trap. Within a single list item, namespaceSelector and podSelector are ANDed: pods with this label in namespaces with that label. As two separate list items, they are ORed: any pod with this label in the policy’s own namespace, plus every pod in the labelled namespaces. One missing indentation level changes “only the ingress controller” into “the entire monitoring namespace and every pod labelled the same locally.” Review policies for this specifically.
Namespaces are matched by label, and Kubernetes sets the immutable label kubernetes.io/metadata.name on every namespace, so you can select a namespace by name without maintaining your own labels. Treat any label that policy depends on as a security control: if developers can edit namespace labels, they can edit your segmentation.
The node exception and ipBlock
The upstream documentation lists one exception worth memorising: traffic to and from the node where a pod runs is always allowed, regardless of the pod’s or node’s IP address. That is how the kubelet reaches your pod for health probes and why local probes survive a default-deny. It is also why node-resident components, a compromised host agent for instance, are outside what NetworkPolicy can contain on that node. Implementations differ on the finer points, so test probes explicitly (more in the failure-modes section).
ipBlock matches CIDRs and supports except lists. It is meant for peers outside the cluster. Using it to describe pod IPs is fragile because pod IPs churn, and because the source address a policy engine sees may be rewritten by SNAT, load balancers or overlay encapsulation, depending on configuration. Prefer selectors for anything inside the cluster, and keep ipBlock for stable external ranges like a corporate VPN or a managed database subnet.
Runnable baseline: default deny plus DNS
The following is the baseline every namespace should receive. It is portable across any conforming CNI. The first policy isolates all pods for both directions; the second restores DNS, which almost every workload needs, using the automatic namespace name label.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: payments
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
Two details deserve explanation. TCP 53 is required alongside UDP because DNS falls back to TCP for large responses and for zone-style lookups, and a policy that allows only UDP produces intermittent failures that look like flaky DNS rather than a policy gap. And the k8s-app: kube-dns label is the conventional label on CoreDNS pods even though the deployment is named CoreDNS; confirm it in your cluster with kubectl -n kube-system get pods --show-labels, because managed distributions and node-local DNS caches change it. If you run NodeLocal DNSCache, pods query a link-local address on the node, and the policy must allow that destination as well.
Allowing the ingress side is where the design work happens. A typical application namespace needs an ingress allow from the ingress controller’s namespace on the service port, an allow from the monitoring namespace on the metrics port, and intra-namespace allows between tiers. Express each as its own small, named policy rather than one mega-policy; small policies diff cleanly in review and can be deleted individually when a service is retired.
Where cluster-wide defaults belong
A per-namespace default-deny has an operational flaw: a namespace created tomorrow has none. Closing that gap takes one of three mechanisms. A namespace-creation controller or GitOps template stamps the baseline into every new namespace. A Kyverno or Gatekeeper-style generation policy does the same reactively. Or the CNI’s cluster-scoped resource (Calico GlobalNetworkPolicy, Cilium CiliumClusterwideNetworkPolicy, or the newer upstream ClusterNetworkPolicy) expresses a cluster-wide default. The first two keep you on portable resources; the third is more complete but couples you to a vendor or an alpha API. We compare these in the analysis section.
Deeper Analysis: Cilium and Calico Compared, Then the Rollout
The two engines converge on the same outcome, an isolated pod with explicit allows, but they diverge on how you discover what to allow, how you rehearse enforcement, and what a policy can express. Everything below reflects the vendors’ current documentation as read in November 2026: the Cilium stable documentation, which references the 1.20 line in its Helm examples, and the Calico Open Source 3.33 documentation. Verify against the release you actually run, because audit and staging behaviors have moved between versions.
Cilium: identity, L7, FQDN, Hubble, audit mode
Cilium assigns each distinct label set an identity and compiles policy into per-endpoint maps keyed on identity, port and protocol. A CiliumNetworkPolicy selects with endpointSelector and describes peers with fromEndpoints, toEndpoints, fromEntities, toCIDR, toFQDNs and so on. Because rules refer to identities rather than IP addresses, pod churn does not trigger rule rewrites, and the cost of a lookup does not grow with the number of rules in the way a long iptables chain does. We make no benchmark claim here; published comparisons vary heavily with kernel, dataplane mode and workload, and you should measure your own.
Cilium’s enforcement mode is a cluster setting with three values documented upstream. In default mode, an endpoint has unrestricted access until a policy selects it, and then enforcement applies per direction. In always mode, every endpoint is enforced even if no rule selects it, which gives you a true cluster-wide default deny but requires you to allow health traffic yourself. In never mode, enforcement is off. You set it with the Helm value policyEnforcementMode or the agent flag enable-policy. always is attractive for a greenfield cluster and dangerous for a retrofit, because it denies infrastructure components at the moment you flip it.
The feature that most separates the two for egress is DNS-aware policy. Kubernetes cannot say “allow this pod to reach api.stripe.com“, and ipBlock cannot track a CDN whose addresses change every minute. Cilium’s toFQDNs does it by running a DNS proxy: pod DNS queries are intercepted, matched against a DNS rule, and the IPs in the responses are learned and mapped to identities that the datapath then allows. The documentation is explicit that the interception is a separate rule you must specify, that toFQDNs does nothing without an L7 DNS rule covering the endpoint, and that the docs strongly recommend intercepting only your cluster DNS service because intercepting other resolvers broadens the trust boundary. Here is a policy adapted from the upstream pattern.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: payments-egress-fqdn
namespace: payments
spec:
endpointSelector:
matchLabels:
app: checkout
egress:
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
"k8s:k8s-app": kube-dns
toPorts:
- ports:
- port: "53"
protocol: ANY
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "api.stripe.com"
- matchPattern: "*.amazonaws.com"
toPorts:
- ports:
- port: "443"
protocol: TCP
Figure 2 traces what happens at runtime. The pod’s query goes to the Cilium DNS proxy, which checks the DNS rule, forwards to kube-dns, records the answer, and installs the mapping before handing the response back. Only then does the pod’s connection to the returned address match the toFQDNs allow.

Figure 2: Cilium toFQDNs flow. The DNS proxy sits on the query path, learns FQDN to IP mappings from answers, and the datapath allows only learned destinations.
This design has consequences. A pod that connects to a hardcoded IP, skipping DNS, is denied even if the IP belongs to an allowed name. A long-lived connection whose DNS entry expires keeps working until it closes, and short TTLs combined with application-level DNS caching can produce an application that holds an address longer than the policy engine remembers it. The Cilium documentation also notes that matchPattern: "*" inserts all IPs from observed responses into the agent’s DNS cache, which is why the broader your wildcard, the more identities and cache entries you create. The troubleshooting guide exposes FQDN identity counts as metrics so you can watch for exhaustion. Wildcards like *.amazonaws.com also admit every AWS customer’s endpoint, including an attacker’s S3 bucket, so FQDN allow lists are a speed bump against exfiltration, not a wall.
For L7, Cilium can restrict an ingress allow to an HTTP method and path. In the upstream example, a rule permits only GET /public, and violations return an HTTP 403 rather than dropping packets, because enforcement happens in a proxy. That is powerful and costly: the flow now traverses an Envoy-based proxy, so you take its latency and CPU, and anything the proxy cannot parse (non-HTTP protocols on the same port, for example) needs separate handling. Use L7 rules on the few services where path-level control pays for itself, such as an internal admin API, not as a blanket default.
Observability is where Cilium’s rollout story is strongest. Hubble records flow verdicts, and the documented audit mode lets you apply a default-deny while still allowing all traffic, logging what would have been dropped as AUDITED. The documentation enables it daemon-wide through the policy-audit-mode key in the cilium-config ConfigMap or the Helm value policyAuditMode=true, or per endpoint with cilium-dbg endpoint config "$ENDPOINT" PolicyAuditMode=Enabled. You then read verdicts with hubble observe flows -t policy-verdict, distinguishing ALLOWED from AUDITED and, once enforcement is on, DENIED. Two honest caveats from the same page: audit mode supports only layer 3 and 4 policies, and the documentation does not recommend it for production. In practice that means using it in staging or on a canary node set, and treating L7 and FQDN rules as needing their own rehearsal.
Calico: tiers, GlobalNetworkPolicy, staged policy, flow logs
Calico’s native model is closer to a firewall rulebase. A GlobalNetworkPolicy is cluster-scoped and can select workloads across namespaces with a namespaceSelector expression; a namespaced NetworkPolicy in projectcalico.org/v3 has the same ruleset syntax with explicit actions, including Allow, Deny, Log and Pass. Unlike upstream, Calico policies have an order, lower numbers evaluated first, and live in tiers. The 3.33 documentation describes three built-in tiers: kube-admin at order 1,000, default at order 1,000,000 which is where Kubernetes NetworkPolicies and unassigned Calico policies live, and kube-baseline at order 10,000,000. Both outer tiers default to Pass. Within a tier that applies to an endpoint, a packet that matches nothing is dropped through an implicit default deny, unless a Pass action or a tier defaultAction: Pass hands it to the next tier.
That Pass semantic is the core of delegated ownership. A platform team can write, in a high-precedence tier, “block all access to the metadata IP, allow DNS, allow monitoring scrape, and pass everything else”, and application teams then own the rest in the default tier through ordinary NetworkPolicy. Nobody has to merge into a shared file. The Calico documentation for default deny advises against a broad global deny that selects everything, because it could break the cluster, and instead shows a GlobalNetworkPolicy whose namespaceSelector excludes the system namespaces it lists: calico-system, kube-public, kube-system and tigera-operator.
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
name: deny-app-policy
spec:
namespaceSelector: >-
kubernetes.io/metadata.name not in
{"calico-system", "kube-public", "kube-system", "tigera-operator"}
types:
- Ingress
- Egress
egress:
- action: Allow
protocol: UDP
destination:
selector: 'k8s-app == "kube-dns"'
ports: [53]
- action: Allow
protocol: TCP
destination:
selector: 'k8s-app == "kube-dns"'
ports: [53]
This is the Calico documentation’s pattern with formatting adjusted. Review the exclusion list against what your installation really runs; an ingress controller, a service mesh control plane or a CSI driver in another namespace will be denied unless allowed explicitly.
For rehearsal, Calico provides staged policies. The 3.33 documentation lists staged counterparts of the global, Kubernetes and Calico policy kinds, applied with kubectl, which evaluate what a policy would do without changing traffic. Their impact is previewed through flow logs: the effect of a staged policy appears in the policies.pending field of generated flow logs, viewable in the Calico Whisker web console. Once the preview looks right you create an identical enforced policy. One difference to keep in mind compared with Cilium’s audit mode: staging is per policy, so you can stage one namespace’s deny while others are enforced, whereas Cilium’s audit mode in its basic form is a daemon or endpoint switch. Staged policies need flow log collection to be useful, and the Whisker console is part of the observability path in current releases; check what your install has enabled before promising yourself a preview.
The cluster-scoped upstream API and its status
SIG Network’s Network Policy API subproject targets the gap that neither Kubernetes nor vendors fill portably: a cluster administrator’s policy that developers cannot override, and a baseline that developers can override. The first design used two resources, AdminNetworkPolicy and BaselineAdminNetworkPolicy, at v1alpha1. According to the subproject’s October 2025 post, the two were nearly identical, BANP was an unnecessarily rigid singleton, and so a single ClusterNetworkPolicy at v1alpha2 replaces both, with a tier field (Admin or Baseline) deciding evaluation order and a priority field that works in both tiers. Actions are Accept, Deny and Pass. The documented evaluation order is Admin tier, then ordinary NetworkPolicy, then Baseline tier, and an Admin-tier Pass delegates to NetworkPolicy.
Treat this as alpha. The same post says existing users can continue with ANP and BANP at v0.1.7, and that the project plans a beta based on the new resource. Calico’s tier documentation shows it already maps ClusterNetworkPolicy onto its built-in kube-admin and kube-baseline tiers, which is a good sign for convergence. For production today I would write the baseline with upstream NetworkPolicy plus your CNI’s native cluster-wide resource, isolate that choice in one directory of your policy repository, and revisit when the API reaches beta and both Cilium and your Calico version support it. Figure 3 shows the evaluation chain both ecosystems are converging on.

Figure 3: Policy precedence in the tiered model. Admin rules are final or pass down to developer NetworkPolicy; unselected pods fall to the baseline tier, then to the cluster default.
The rollout workflow: observe, generate, rehearse, enforce
The safest sequence for kubernetes microsegmentation is a loop run once per namespace, not a big-bang. Figure 4 captures it.

Figure 4: The per-namespace rollout loop. Rehearsal feeds back into generation until the audit shows no legitimate traffic would be dropped.
Stage 0, prerequisites. Confirm enforcement works with a throwaway namespace. Inventory what runs cluster-wide that policy can break: DNS, the ingress controller, service mesh sidecars and control plane, admission webhooks, metrics scrapers, CSI and CNI daemons, and the API server path. Decide ownership: platform owns cluster-wide guardrails; each namespace owner owns their namespace policies.
Stage 1, observe. Collect flows for at least one full business cycle, which for batch systems means including the monthly job, the weekly report and the failover test. On Cilium, Hubble flow data answers who talks to whom on which port; on Calico, flow logs do the same, and in either case a service mesh’s metrics or an eBPF-based observability tool can fill gaps. A week is a lower bound for most systems. Seasonal traffic you did not see will be the outage.
Stage 2, generate. Turn observed flows into candidate policies as code and then edit them by hand. Cilium’s documentation walks a manual version of this: read the audited verdict to find source labels, destination labels, direction and port, then write a policy matching exactly that. Community tools can automate drafts from Hubble output, but treat generated policy as a proposal. Auto-generation faithfully captures every flow it saw, including the ones that should never have existed, such as a debug pod’s traffic to production, and it misses rare flows. A reviewer should reject allows nobody can justify.
Stage 3, rehearse. Apply the default-deny in audit or staged mode and watch for would-be drops. The target is zero would-be drops on legitimate paths for several days, including during a deployment, a node drain and a restart of the ingress controller, because those events reveal startup-order and probe-path flows that steady state hides. Remember Cilium audit mode is L3/L4 only, so rehearse FQDN and L7 rules on a canary node pool separately.
Stage 4, enforce. Flip one namespace, with a named person watching drop metrics and application error rates, and a prepared rollback that is a single kubectl delete of the default-deny object (keep the allow policies; they are harmless alone). Choose the order by blast radius: start with low-stakes namespaces, then internal services, then the data tier, and leave shared platform namespaces for last and treat them with care. After each enforcement, wait through at least one full traffic cycle before moving on.
CI tests for network policy
Rehearsal in a live cluster is necessary but not sufficient, because policy regressions arrive via pull requests. Three layers of tests catch them. Static linting validates YAML structure and catches the one-dash selector bug and missing policyTypes. Policy-intent unit tests assert connectivity expectations against a simulator or against the rendered policy set: “frontend may reach api on 8080; frontend may not reach db on 5432”. And an ephemeral-cluster job, for example kind with Cilium or Calico installed, deploys test pods and runs actual connectivity checks, failing the build if an expected-allowed flow breaks or an expected-denied flow opens. The Kubernetes-community network policy conformance tests and the connectivity-test tooling shipped with Cilium are useful starting points. Keep the expectations in a small table checked into the repository so that a reviewer can read the intended matrix next to the diff.
Here is a minimal connectivity smoke test that fits any CI runner with kubectl, using only the default-deny and DNS baseline above.
set -euo pipefail
NS=policy-ci
kubectl create ns $NS
kubectl -n $NS run server --image=nginx:stable --port=80 --labels=app=server
kubectl -n $NS expose pod server --port=80
kubectl -n $NS wait --for=condition=Ready pod/server --timeout=90s
# Before policy: client must reach server
kubectl -n $NS run client --rm -i --restart=Never --image=curlimages/curl \
--labels=app=client -- curl -s -m 3 -o /dev/null http://server
kubectl apply -n $NS -f default-deny-all.yaml
# After deny: the same call must now FAIL
if kubectl -n $NS run client2 --rm -i --restart=Never --image=curlimages/curl \
--labels=app=client -- curl -s -m 3 -o /dev/null http://server; then
echo "default deny not enforced" >&2; exit 1
fi
echo "default deny verified"
kubectl delete ns $NS
The first call proves the cluster works at all, which prevents a false pass when DNS or the image pull is broken; the second proves enforcement, which prevents the silent-no-op failure of a CNI that ignores policy.
Performance and scale considerations
Every enforcement model has a cost curve, and it is worth knowing which one you are on. Rule evaluation in iptables-based datapaths scales with chain length, and large clusters with thousands of services have historically seen slow programming of rules on churn. eBPF datapaths replace linear chains with map lookups, which flattens evaluation cost but moves the pressure elsewhere: map sizes, identity counts, and agent memory. For Cilium, the FQDN features add identity allocation per learned destination and DNS cache size; for L7 you add proxy hops. Calico’s policy programming cost grows with the number of selectors and endpoints, and its documentation notes that policy tiers are evaluated in order, so deeply layered rulebases cost more than flat ones.
The scale risks that actually hurt are control-plane ones. A policy with a broad selector in a cluster that creates and destroys thousands of pods per minute forces every agent to recompute. Label cardinality matters too: if you stamp a unique label on each pod, such as a hash or a build ID, and select on a rule that includes it, identity-based engines allocate a new identity per label set, and the policy engine churns. Keep selector labels stable and few: app, tier, environment, team. Measure agent CPU and policy-programming latency in a staging cluster sized like production before enforcing cluster-wide, and treat those numbers as yours, not as vendor claims.
Trade-offs, Gotchas, and What Goes Wrong
Most default-deny outages come from a short list of causes. Knowing them in advance turns a surprise into a checklist item.
DNS blocked. This is the most common failure by a wide margin. Egress default-deny without a DNS allow breaks name resolution instantly, and the symptoms (timeouts, no such host, readiness failing) look like an application bug. Always ship the DNS allow in the same commit as the deny. Check for NodeLocal DNSCache, which changes the destination, and for TCP fallback on port 53. With Cilium FQDN rules, remember the separate DNS visibility rule; without it the toFQDNs allow does nothing.
Health probes and node traffic. Upstream documents that traffic to and from the pod’s own node is always allowed, so kubelet probes normally survive. But implementations differ in edge cases: probes through a service mesh sidecar, probes from a load balancer’s health checker straight to a pod IP in native-routing setups, and Cilium’s always enforcement mode, where the documentation advises allowing health endpoint traffic. If a pod flaps ready/unready right after enforcement, suspect probes first. Cilium’s host-level entities, such as host and remote-node, and Calico’s host endpoint handling are the vendor-specific levers.
Webhooks and API server reachability. Admission webhooks run in pods and the API server calls them from the control plane, often from an address outside any pod selector. Deny the webhook’s ingress and every create of a matched resource starts failing, which may include the pods you need to fix the policy. Conversely, controllers and operators call the API server from inside the cluster; their egress needs an allow to the API server, which with Cilium can be expressed with the kube-apiserver entity, and with Calico by selector or CIDR. Set webhook failurePolicy deliberately and keep a break-glass role that can delete NetworkPolicy objects.
Ingress controllers and external traffic identity. Cilium’s own caveats page shows that the identity assigned to external requests to a service depends on routing mode: with tunneling they appear as reserved:world; in native routing they appear as reserved:world only if the load balancer delivers to the node hosting the endpoint, otherwise as reserved:remote-node, which matches fromEntities: cluster. The same client traffic can therefore match different rules depending on which node the load balancer picked. Test through the real load balancer path, not a port-forward, and check externalTrafficPolicy assumptions.
Selector mistakes. The ANDed versus ORed peer bug, labels that do not exist on the target, a namespace label that developers can edit, and podSelector: {} where a narrower selector was intended. A policy that selects zero pods is silent and a policy that selects too many is loud; the first is worse.
FQDN limits. Hardcoded IPs bypass DNS rules, DNS-over-HTTPS from an application circumvents the DNS proxy entirely, wildcard patterns allow unintended tenants of a shared cloud domain, and short TTLs create race conditions in long-lived clients. FQDN policy narrows egress; it does not guarantee that data cannot leave. Pair it with egress gateways, proxies or private endpoints when exfiltration is a real concern.
What NetworkPolicy cannot do. It does not authenticate workloads, encrypt traffic, restrict by service account, or constrain traffic that originates on a node, and it has no logging of its own. For workload identity use mTLS from a service mesh or Cilium’s mutual authentication features, evaluated for maturity in your version. Segmentation and identity are complementary: network policy shrinks reachability; mTLS proves who is talking.
Operational debt. A policy set is code that must track the application. Unowned policies rot: services are retired while their allows remain, creating a growing set of unexplained holes. Require every policy to carry an owner annotation and a ticket reference, and prune on a schedule. Finally, resist the urge to put everything in default-deny on day one in infrastructure namespaces such as kube-system; the Calico guidance to exclude system namespaces from a global deny exists for a reason, and a staged, specific policy for those namespaces is safer than a blanket one.
Practical Recommendations
If you are starting a rollout this quarter, pick the lightest path that gives you rehearsal. On Cilium, use Hubble for observation and the documented audit mode in a staging or canary environment, and keep production changes per namespace. On Calico, use flow logs and staged policies, and place cluster-wide guardrails in a high-precedence tier so application teams can own the rest. In both cases write the baseline in upstream NetworkPolicy and keep vendor resources for what upstream cannot express: FQDN egress, L7 rules, cluster-wide guardrails.
Order matters more than tooling. Enforce egress and ingress together for a namespace, but only after the DNS, ingress-controller, monitoring and API-server allows are present and tested. Prefer many small named policies with an owner annotation over one large file. Treat labels used by policy as security-sensitive, and restrict who can edit namespace labels with admission policy. Keep a break-glass procedure that is documented, rehearsed and available to on-call without a pull request.
Finally, instrument. Alert on sustained increases in policy drops per namespace, on new AUDITED or pending verdicts after a deploy, and on DNS error rates. A default-deny cluster without drop visibility will break quietly. A good checklist for each namespace:
- [ ] Enforcement verified in the cluster with the connectivity smoke test (allowed before, denied after).
- [ ] At least one full business cycle of flows observed and reviewed.
- [ ] Default-deny for both directions committed together with the DNS allow (UDP and TCP 53, correct DNS labels).
- [ ] Allows present for ingress controller, monitoring scrape, API server access where needed, and intra-namespace tiers.
- [ ] Audit or staged mode run for several days with zero would-be drops on legitimate paths, including during deploys and node drains.
- [ ] Rollback is a single deletion of the deny object, tested, and on-call knows it.
- [ ] Drop-rate and DNS-error alerts active before enforcement.
- [ ] New-namespace template or cluster-wide guardrail so tomorrow’s namespace is not open.
- [ ] Every policy has an owner annotation and a review date.
Frequently Asked Questions
What is a default-deny network policy in Kubernetes?
It is a NetworkPolicy that selects every pod in a namespace with podSelector: {} and lists the policy types Ingress, Egress, or both without any allow rules. Selection makes the pods isolated in those directions, so only traffic permitted by other policies is allowed. The object does nothing unless your CNI enforces NetworkPolicy. You then add small allow policies for DNS, ingress controllers, monitoring and each real service-to-service flow.
Does default deny break DNS and health checks?
It breaks DNS if you deny egress without allowing UDP and TCP port 53 to your cluster DNS pods, so always ship the DNS allow with the deny. Kubelet health probes normally keep working because upstream documents that traffic to and from a pod’s own node is always allowed, but probes via sidecars, load balancers or Cilium’s always mode can differ. Test probes during audit, not after enforcement.
Should I choose Cilium or Calico for microsegmentation?
Choose on the capabilities you need, not on brand. Cilium offers identity-based enforcement in eBPF, L7 HTTP rules, DNS-aware toFQDNs, and Hubble verdicts with audit mode. Calico offers tiers with ordered evaluation and Pass, GlobalNetworkPolicy, and staged policies previewed through flow logs. Both enforce upstream NetworkPolicy, so a portable baseline works on either. If you need FQDN or L7 control, Cilium fits; if you need delegated ownership across teams, Calico tiers fit.
How do I test network policies safely before enforcing?
Observe real flows first, write candidate policies, then rehearse without dropping traffic. On Cilium, enable policy audit mode and read AUDITED verdicts with hubble observe flows -t policy-verdict; the documentation notes it covers only L3 and L4 and is not recommended for production. On Calico, apply staged policies and inspect pending impact in flow logs. Add CI connectivity tests in an ephemeral cluster so regressions fail before merge.
What is the difference between NetworkPolicy and AdminNetworkPolicy?
NetworkPolicy is namespaced, owned by developers, additive allow-only, and stable. AdminNetworkPolicy and BaselineAdminNetworkPolicy were alpha cluster-scoped resources with Allow, Deny and Pass actions that let administrators set precedence above or below NetworkPolicy. SIG Network has announced that ClusterNetworkPolicy at v1alpha2 replaces both, using a tier field and Accept, Deny and Pass actions. Because it remains alpha, verify support in your CNI version before depending on it.
Can default deny stop data exfiltration?
It substantially reduces it by limiting where a compromised pod can connect, but it does not eliminate it. Allowed destinations can still be abused, wildcard FQDN rules admit unrelated tenants of shared cloud domains, DNS tunnelling or DNS-over-HTTPS can bypass name-based controls, and node-originated traffic is outside pod policy. Combine default deny with narrow egress rules, egress gateways or proxies, private endpoints, workload identity, and flow monitoring.
Further Reading
- CNI comparison: Calico, Cilium, Flannel and Multus on Kubernetes for datapath and operational differences.
- Cilium vs Calico for industrial Kubernetes networking, an ADR for a decision record in constrained environments.
- Kubernetes NetworkPolicy egress to RDS, a practical guide for a worked external-database egress case.
- Kubernetes documentation: Network Policies, the upstream semantics.
- Cilium documentation: Layer 7 policies and DNS and Creating Policies from Verdicts.
- Calico documentation: default deny and policy tiers.
- Network Policy API: ClusterNetworkPolicy replaces AdminNetworkPolicy and BaselineAdminNetworkPolicy.
By Riju — about
