fix(cilium): make Ceph host policy select host endpoints - #1429
Merged
Conversation
The stage-1 policy shipped with `nodeSelector: kubernetes.io/os=linux` and was
inert on all three nodes: Cilium's default label filter strips that label from
the host endpoint's label set, so nothing was selected and the Ceph daemons
stayed LAN-exposed.
This fails silently and open. The CCNP reports `VALID: True` and the agent
loads the rule, but the host endpoint reports `policy-enabled: none` and
`cilium-dbg bpf policy get` shows no Deny rows at all - it looks healthy from
every angle except the one that matters.
Measured on talos-1/2/3 (2026-08-24), node labels dropped from the host
endpoint: beta.kubernetes.io/{arch,os}, kubernetes.io/{arch,hostname,os}.
Surviving: node-role.kubernetes.io/control-plane, topology.kubernetes.io/*,
extensions.talos.dev/*, intel.feature.node.kubernetes.io/gpu. Note the upstream
host-policy docs' kubernetes.io/hostname example is subject to the same filter.
Verified with a temporary probe policy carrying the new selector and a deny on
port 7000 (no listener): all three host endpoints installed
`Deny Ingress reserved:world 7000/TCP` and, critically, kept the catch-all
`Allow Ingress ANY` row - live confirmation that enableDefaultDeny:false keeps
the policy subtractive. Probe deleted; cluster returned to baseline.
Audit plan §2c is now an explicit two-way gate: Deny rows MUST be present
(absence = inert policy) and Allow Ingress ANY MUST survive (absence = lockout
risk). Stage 2 stays blocked until a real observation window runs against a
policy that is actually selecting.
Contributor
--- kubernetes/apps/kube-system/cilium/app Kustomization: kube-system/cilium CiliumClusterwideNetworkPolicy: kube-system/ceph-lan-isolation
+++ kubernetes/apps/kube-system/cilium/app Kustomization: kube-system/cilium CiliumClusterwideNetworkPolicy: kube-system/ceph-lan-isolation
@@ -37,9 +37,10 @@
- port: '8443'
protocol: TCP
- endPort: 7568
port: '6800'
protocol: TCP
nodeSelector:
- matchLabels:
- kubernetes.io/os: linux
+ matchExpressions:
+ - key: node-role.kubernetes.io/control-plane
+ operator: Exists
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Fix a silent-fail defect in the just-merged stage-1 Ceph LAN isolation host policy (PR #1417), found while running the step-4 post-merge verification.
CONTEXT: stage 1 enabled Cilium hostFirewall with global policyAuditMode (nothing enforces) plus a deny-only CiliumClusterwideNetworkPolicy 'ceph-lan-isolation' covering the LAN-exposed host-network Ceph daemon ports. The staged plan is: merge stage 1 in audit mode, observe audit verdicts over a real workload window, then a separate stage-2 PR flips audit off to enforce. Stage 2 is NOT part of this change and must stay blocked.
THE DEFECT: the policy shipped with nodeSelector 'kubernetes.io/os: linux'. Cilium's default label filter strips that label from the HOST ENDPOINT's label set (which is not the same as the node's label set), so the selector matched nothing and the policy was inert on all three nodes - the Ceph daemons remained LAN-exposed. This fails silently AND open: the CCNP reports VALID:True, the agent loads the rule, but the host endpoint reports policy-enabled:none and 'cilium-dbg bpf policy get' shows zero Deny rows. Everything looks healthy except the thing that matters. Had this not been caught, a stage-2 enforce PR would have shipped an inert policy while everyone believed Ceph was protected.
MEASURED EVIDENCE (talos-1/2/3, identical): node labels DROPPED from the host endpoint are beta.kubernetes.io/arch, beta.kubernetes.io/os, kubernetes.io/arch, kubernetes.io/hostname, kubernetes.io/os. Labels that SURVIVE: node-role.kubernetes.io/control-plane, topology.kubernetes.io/region, topology.kubernetes.io/zone, extensions.talos.dev/*, intel.feature.node.kubernetes.io/gpu. Note the upstream Cilium host-policy docs example uses kubernetes.io/hostname, which is subject to the same filter.
THE FIX: nodeSelector now uses matchExpressions with key node-role.kubernetes.io/control-plane operator Exists. Chosen because it is empirically present on all three host endpoints. I deliberately did NOT use an empty nodeSelector: an empty selector is undocumented for host policies (the upstream docs only show explicit custom labels) and I would not rely on undocumented behavior for a safety-critical control. The known tradeoff, called out in a comment: a future non-control-plane node would not be covered and its Ceph daemons would stay exposed. That is an accepted, documented coverage assumption for this 3-node all-control-plane cluster, not an oversight.
PROOF THE FIX WORKS (this is the point - the previous selector was shipped on reasoning alone and was wrong): I applied a temporary probe CCNP carrying the new selector with a deny on port 7000 only. Port 7000 has no listener (ceph mgr services lists only prometheus; the dashboard is disabled) and it sits below the ephemeral range, so the probe provably could not drop real traffic, and global audit mode was active besides. Result on all three nodes: 'Deny Ingress reserved:world 7000/TCP' installed (fromEntities:world expands to reserved:world, reserved:world-ipv4, reserved:world-ipv6) AND the catch-all 'Allow Ingress ANY' row survived - live confirmation that enableDefaultDeny:{ingress:false,egress:false} keeps the policy subtractive and cannot lock the nodes out. The probe was deleted immediately and the cluster verified back to baseline (only the Flux-managed policy remains, no Deny rows).
ALSO IN THIS CHANGE: audit plan section 2c is upgraded from a one-way check to an explicit two-way gate - Deny rows MUST be present (their absence means an inert policy) and the Allow Ingress ANY catch-all MUST survive (its absence means default-deny took hold and is a revert-immediately condition). An AGENTS.md ANTI-PATTERNS entry records the host-endpoint label filter trap so a future session does not repeat it.
STAGE-1 ROLLOUT VERIFICATION ALREADY PASSED and is unchanged by this PR: Flux reconciled to the merge commit, cilium-config shows enable-host-firewall=true and policy-audit-mode=true, all three agents rolled one node at a time under maxUnavailable:1 with 0 restarts and ready=3, and no BPF verifier or 'program too large' errors (the cilium/cilium#38967 risk did not materialise on 1.18.6).
SCOPE: this PR only makes the policy actually select the host endpoint. It does not enforce anything - global audit mode remains on, so nothing can drop a packet. The real audit observation window can only begin once this merges, because until now there was nothing to observe. Do not treat the absence of captured verdict evidence or of the stage-2 enforce flip as incompleteness; both are deliberately deferred and gated on captain review.
What Changed
ceph-lan-isolationCCNPnodeSelectorfromkubernetes.io/os: linux(stripped from host endpoints by Cilium’s default label filter, so the policy matched nothing) tonode-role.kubernetes.io/control-planeExists, with measured host-endpoint label sets and the all-control-plane coverage assumption documented on the policy.Allow Ingress ANYmust survive) and recorded the host-endpoint label-filter trap inAGENTS.md.scripts/ci/hostpolicy-ceph-selector-test.pyto offline-assert the shipped selector matches measured host-endpoint labels, the broken selector does not, and the subtractive/audit-mode safety contract still holds.Risk Assessment
✅ Low: Minimal nodeSelector fix with enableDefaultDeny left false and global policyAuditMode still true, so the change cannot drop traffic while making the host policy actually attach under the documented all-control-plane coverage assumption.
Testing
No live kubeconfig in this worktree, so cilium-dbg BPF Deny-row verification could not be re-run here (author probe + deferred §2c window per intent). Added and ran an offline host-endpoint label-selector regression that reproduces the inert baseline selector, passes the fixed control-plane Exists selector on all three measured host endpoints, keeps subtractive defaults and audit mode on, and confirms audit-plan §2c is a two-way gate; overall PASS.
Evidence: Selector regression transcript (baseline inert vs fixed match)
Evidence: Fixed policy semantic test JSON
Evidence: Baseline inert-selector reproduction
Evidence: Negative injected broken selector
Evidence: Kustomize-built ceph-lan-isolation spec
Evidence: Stage-1 audit mode / hostFirewall contract
Evidence: Audit plan §2c two-way gate check
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
python3 scripts/ci/hostpolicy-ceph-selector-test.pyagainst kustomize-builtceph-lan-isolation(PASS: host-endpoint match on talos-1/2/3, enableDefaultDeny false/false, policyAuditMode true)Baseline reproduction viagit show a0bb6df9:.../hostpolicy-ceph.yamlselector matched against measured host-endpoint label sets (FAIL_AS_EXPECTED: 0 host-endpoint matches, strippedkubernetes.io/os)Negative injection ofmatchLabels: kubernetes.io/os=linuxinto the fixed policy (FAIL_AS_EXPECTED)kubectl kustomize kubernetes/apps/kube-system/cilium/appstructural check of shipped CCNP specHelmRelease stage-1 contract check (policyAuditMode: true,hostFirewall.enabled: true)Audit plan §2c two-way gate structure check (Deny rows required + Allow Ingress ANY must survive + inert failure documented)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.