Skip to content

fix(policies): add-default-deny has generated an invalid CiliumNetworkPolicy into every namespace for ~101 days #3501

Closed
1 of 1 issue completed

Description

@devantler

🤖 Generated by the Agentic Engineer

Evidence

The add-default-deny ClusterPolicy's generate-default-deny rule has never produced a valid resource. Every CiliumNetworkPolicy/default-deny it generates is rejected by Cilium:

status.conditions[Valid] = False
message: "rule must have at least one of Ingress, IngressDeny, Egress, EgressDeny"

Measured live in prod (2026-09-01): 39 invalid default-deny CiliumNetworkPolicies — one in every namespace the rule targets, and every single one invalid. Unfiltered control: of all CNPs cluster-wide, the only ones with Valid=False are these 39; every allow-* CNP is Valid=True. Oldest such object dates to 2026-05-23, so the rule has been generating a rejected resource for roughly 101 days.

The cause is stated in the policy's own comment, which is mistaken about the CRD:

Cilium's CRD also requires at least one policy direction to be present, so retain empty allow lists for both directions.

ingress: [] / egress: [] are empty arrays. Cilium requires at least one direction to be present with a rule, and an empty list does not satisfy that — so the whole object is rejected, enableDefaultDeny included.

Impact — no open hole today, but one brace of a two-brace design is missing

Enforcement is currently intact, and I want to be precise about that rather than overstate this. The sibling rule generate-default-deny-networkpolicy generates a standard Kubernetes NetworkPolicy (podSelector: {}, policyTypes: [Ingress, Egress], no allow rules), which Cilium does enforce. Verified: 39 of 43 namespaces carry it; the 4 without are kube-system, kube-public, kube-node-lease and kyverno, which the policy deliberately excludes. So namespaces are isolated today.

What is actually broken:

  1. The documented design is half-missing. The policy describes a deliberate belt-and-braces arrangement — the Cilium policy activates whitelist mode, the standard NetworkPolicy makes segmentation visible to scanners and CIS (C-0030/C-0260/C-0206). The Cilium brace has never existed. Anyone who trims the "redundant" standard NetworkPolicy on the strength of the comment would open a real gap.
  2. 39 permanently-invalid objects in prod. Any check asking "are there invalid network policies?" is permanently dirty, which trains readers to ignore it — a guard that always fires is one nobody reads.
  3. It has propagated. The identical file exists in devantler-tech/platform-template at the same path, so every platform created from the template inherits the broken rule.

Expected

generate-default-deny produces a CNP that Cilium accepts (Valid=True) and that activates default-deny by endpoint selection, without introducing an explicit ingressDeny/egressDeny — the policy comment is right that an explicit deny would take precedence over each namespace's allow policies.

⚠️ Hazard — this change can take prod down, and has before

A deny-shaped Cilium policy change caused a live prod egress outage in this cluster (CoreDNS to host DNS was cut in under 10 seconds, and a render-only test passed beforehand). enableDefaultDeny defaults to true for any direction that carries rules, so getting this subtly wrong revokes egress cluster-wide across all 39 namespaces at once. This is explicitly not a quick fix to push:

  • prove the corrected shape on a throwaway namespace on a local/dev cluster first, asserting Valid=True and that an existing allow policy still admits its traffic;
  • a render/schema test is not sufficient evidence — the previous outage passed exactly that;
  • stage the rollout rather than flipping all namespaces at once.

Acceptance criteria

  • generate-default-deny emits a CNP that Cilium reports Valid=True.
  • Existing per-namespace allow-* CNPs still admit their traffic (DNS egress specifically verified).
  • Zero Valid=False CiliumNetworkPolicies remain in prod.
  • The misleading "retain empty allow lists" comment is corrected to describe what the CRD actually requires.
  • The same fix lands in devantler-tech/platform-template.
  • A check fails if any generated CNP is Valid=False, so this cannot silently regress for another 101 days.

Rough size: S–M for the policy change; the validation and staged rollout are the bulk of the work.

Activity

  1. added theissue type on Sep 1, 2026
  2. moved this to 📥 Backlog in 🌊 Project Boardon Sep 1, 2026
  3. devantler commented on Oct 2, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Re-verified 2026-10-02. The fix shape is confirmed and it cannot narrow traffic, but the hazard gate above still blocks a push from this agent host.

    Still broken. A read-only prod read today finds 167 CiliumNetworkPolicies. 39 are Valid=False, and all 39 are default-deny, with the same spec as when this issue was filed (ingress: [], egress: []).

    The corrected shape. Cilium's Layer 3 docs ("Ingress/Egress Default Deny") put an endpoint into default deny for a direction with a rule section holding one empty rule: ingress: [{}] / egress: [{}]. That section allows no peers. It also satisfies the validator ("at least one of Ingress, IngressDeny, Egress, EgressDeny"), and it needs no ingressDeny or egressDeny.

    Why it cannot narrow traffic in prod. All 39 namespaces that carry the invalid CNP also carry the standard default-deny NetworkPolicy (podSelector: {}, Ingress + Egress), measured today, 39 of 39. Cilium already enforces default deny in both directions there, and allows are a union across policy types. A valid CNP that allows nothing therefore leaves the allowed set and the enforcement mode as they are. If the empty rule were ever read as allow-all, the failure would be widening, not an outage. That is still why the live proof matters.

    Blocker. The hazard section requires proving the shape on a throwaway cluster before rollout. This host has no container runtime. Neither platform's CI nor platform-template's runs an ephemeral cluster: the template's System Test is skipped in the template repository. So nothing reachable from here can run the proof.

    Cheapest ways to unblock:

    • run the proof on a host with Docker: a ksail Talos/Docker cluster with Cilium 1.20.2 and Kyverno, plus this policy;
    • or add a dispatch-only workflow that does the same on a GitHub runner;
    • or decide here that the 39/39 precondition replaces the throwaway-cluster proof.

    Once unblocked, the change is the two lines above, a corrected comment, and a static check in validate that fails on any generated CiliumNetworkPolicy with no non-empty direction.

  4. added a commit that references this issue on Oct 2, 2026
  5. moved this from 📥 Backlog to 👀 In Review in 🌊 Project Boardon Oct 2, 2026
  6. devantler commented on Oct 3, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    The explicitly approved disposable proof has now cleared the issue’s pre-production hazard gate for candidate head 01e40ec using reviewed main workflow head 42d5010: https://github.com/devantler-tech/platform/actions/runs/37102114892

    The finite receipt reports verdict PASS and cleanup PASS on Cilium 1.20.2. Cilium accepted the generated default-deny and both allow policies as valid. DNS and explicitly allowed HTTP remained reachable; unallowed ingress, egress, and target HTTP were denied. The pre-floor control confirmed the HTTP path was reachable before the deny floor was applied.

    This removes the throwaway-cluster blocker, but does not claim completion. Remaining acceptance is the same fix in devantler-tech/platform-template, current-main integration of platform PR #4381, staged production rollout, and a live readback showing zero invalid CiliumNetworkPolicies.

  7. moved this from 👀 In Review to ✅ Done in 🌊 Project Boardon Oct 3, 2026
  8. devantler commented on Oct 3, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Post-merge production readback for #4406 (fc2cca1a197adee3cc5b9ae113009c0cd9f93ab5):

    • protected merge-group CI and its production deployment completed successfully;
    • the live Kyverno add-default-deny policy reports Ready=True and contains the expected generated Cilium shape;
    • all 39 live default-deny CiliumNetworkPolicies use ingress: [{}] and egress: [{}] with both default-deny flags enabled;
    • zero live policies retain the invalid empty-list shape;
    • all 199 Cilium endpoints and all 11 Flux kustomizations report Ready; and
    • no CiliumNetworkPolicy warning events were emitted after the deployment.

    The disposable traffic proof remains independently green from run 37102114892, including cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    None yet

    Effort

    None yet

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions