Skip to content

feat(policy): reject explicit hostUsers opt-out in userns namespaces#2691

Merged
devantler merged 2 commits into
mainfrom
claude/policy-userns-enforce-ratchet
Jul 18, 2026
Merged

feat(policy): reject explicit hostUsers opt-out in userns namespaces#2691
devantler merged 2 commits into
mainfrom
claude/policy-userns-enforce-ratchet

Conversation

@devantler

Copy link
Copy Markdown
Contributor

🤖 Generated by the Daily AI Engineer

Why

The platform's user-namespace hardening can be switched off silently. It is applied by a mutate rule that only fills in the setting when a workload hasn't specified one — so any workload that explicitly asks for the old, less-isolated behaviour simply keeps it. No error, no report, nothing surfaced. The namespace still carries its "hardened" label and still looks correct.

Defence-in-depth that can degrade invisibly is worse than none, because it reports as present while being absent.

What

Adds an admission rule that rejects a workload explicitly opting out of user namespaces in a namespace that has graduated through a user-namespace pilot — the same guard the platform already applies to the other host-isolation settings. Databases keep their existing exemption, and workloads that say nothing are unaffected.

Verified against live production: all five opted-in namespaces are already compliant, so nothing running today is blocked by this.

Fixes #2690
Part of #1807

The user-namespace rollout is mutate-only: add-security-context.yaml injects
hostUsers: false through a +(hostUsers) conditional anchor, which only fills in
an absent value. A workload that explicitly sets hostUsers: true keeps it,
silently, in a namespace that still carries the opt-in label.

Add a disallow-host-users validate rule mirroring the mutate rule's scope (same
namespaceSelector, same cnpg.io/cluster exclude) and the file's existing
=(hostNetwork): false idiom, so an absent value passes and an explicit true is
rejected at admission.

Part of #1807
Nothing executed the fixtures under tests/, so tests/validate-replica-floor had
never actually run in CI and a policy regression could ship green. Add a
'kyverno test ./tests' step to the existing static validate job and broaden the
k8s paths filter from the single cilium fixture to tests/**.

The CLI is pinned to v1.18.2, the version the kyverno Helm release runs, so a
policy that passes here is evaluated by the same engine that admits it in
production.

Also add values.yaml to the new fixture: the CLI does not read namespace labels
from Namespace objects in resources.yaml, so a namespaceSelector rule reports
every resource 'Excluded' and the whole fixture — including its fail case —
passes vacuously without it.

Part of #1807
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Daily AI Engineer

Validation record

Engine parity. Local CLI and the pinned CI CLI are both v1.18.2, matching the kyverno Helm release running in production — so every result below comes from the same engine that admits pods on the cluster.

RED/GREEN — the fixture has teeth

The first version of this fixture passed vacuously and I nearly shipped it. kyverno test reported 5 tests passed, but every row's REASON was Excluded: the CLI does not read namespace labels from Namespace objects in resources.yaml, so a namespaceSelector-matched rule never applies to anything — including the fail case. It would have passed with the rule deleted. values.yaml fixes it; rows 1–3 now report Ok, rows 4–5 Excluded by design.

Two negative controls prove the assertions can fail:

control result
invert the pattern (=(hostUsers): falsetrue) 2 failed, exit 1
remove the cnpg.io/cluster exclude cnpg pod: "Want skip, got fail"1 failed, exit 1
restored 5 passed, exit 0

A third control confirms the CI invocation fails closed: kyverno test ./tests returns exit 1 on a broken policy and 0 when restored — worth checking explicitly, since kyverno apply exits 0 on failures by default.

CI wiring

kyverno test ./tests runs all fixtures: 12 passed (5 new + 7 pre-existing validate-replica-floor), with the non-Kyverno cilium-bandwidth-manager-bbr directory correctly ignored. This is the first time validate-replica-floor has ever executed in CI — it passes, so wiring it in breaks nothing.

actionlint clean. kustomize build renders 22 ClusterPolicies including the new rule. cspell flags userns/devantler in the new files, but the pre-existing add-security-context.yaml flags 24 issues under the same config — these paths aren't gated, so this matches the repo baseline rather than being a new regression.

Tried and evaluated as a user — against live production

Exported all 15 running pods from the five opted-in namespaces and ran the policy against their real specs:

  • unmodified → pass: 40, fail: 0 — no workload currently running in production is blocked by this rule, so it is safe at Enforce.
  • 10 pods tampered to hostUsers: truefail: 10, exactly the tampered ones, each with the real admission message. A clean 1:1 correspondence — the guard catches the exact regression it exists for, on real workloads.

Scope note

The cnpg.io/cluster exclude is deliberately redundant for today's CNPG pods (they leave hostUsers absent, which =() already passes). It mirrors the mutate rule so the validate rule can never block a workload the mutate rule deliberately skipped — including if CNPG later sets the field explicitly.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Daily AI Engineer

@codex review for admission-policy correctness: the =(hostUsers): false validate-if-present semantics, whether the cnpg.io/cluster exclude and namespaceSelector scope match the mutate rule in add-security-context.yaml exactly, and whether adding an Enforce rule to an existing Enforce policy can block any workload not covered by the live-pod evaluation recorded above.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: ba6636aade

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@devantler
devantler marked this pull request as ready for review July 18, 2026 11:09
@devantler
devantler added this pull request to the merge queue Jul 18, 2026
Merged via the queue into main with commit 9f7aa17 Jul 18, 2026
20 checks passed
@devantler
devantler deleted the claude/policy-userns-enforce-ratchet branch July 18, 2026 11:16
@github-project-automation github-project-automation Bot moved this from 🫴 Ready to ✅ Done in 🌊 Project Board Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

Guard the user-namespace opt-in against silent regression (enforce ratchet)

1 participant