Implements the credential model you chose: a narrow trigger SA bounded by a fail-closed
Kyverno admission policy.
The escalation this closes
Kubernetes places no use check on a Job's serviceAccountName. Anyone who can create jobs in ns conformance can create a Job running as conformance-runner
(nodes/patch, pods/exec, namespaces/patch) with an arbitrary command. A CI token with plain create jobsis that privileged identity, however narrow its own Role looks. RBAC cannot
fix this — only constraining the shape of what the trigger may create.
Verified with kyverno apply (pass 10, fail 6, error 0), not assumed
Case
Result
evil-command — right SA, right image, arbitrary command
DENIED at /containers/0/command/ ← the attack
evil-image
DENIED at /image/
tag-not-digest — mutable tag is republishable
DENIED at /image/
host-namespaces
DENIED at /hostPID/
extra-volume — the way out of a constrained pod
DENIED
injected-args
DENIED
good-run
ADMITTED
The policy is Enforce + failurePolicy: Fail and deliberately does not use ${KYVERNO_VALIDATION_ACTION} — a boundary an overlay can downgrade to Audit is not a boundary.
It constrains onlyconformance-trigger, so a mistake here can't wedge the cluster.
⚠️ It also fixes a fail-open in the gate itself
kyverno test does not fail when a policy cannot LOAD. Every row becomes Skip / Invalid Policy and it still prints "N tests passed" and exits 0. I hit this while
writing the policy — a schema error produced exactly that, and ci/l0.sh would have certified a security policy that does not load as green.
Not specific to this policy: a typo in restrict-host-namespaces or secret-provenance would
silently disable it with the gate still green. ci/l0.sh now scans for Invalid Policy.
Proven: broken policy → l0 rc=1; restored → rc=0.
c2-root-surface
The new Kustomization reconciles a tree that check doesn't scan, so it failed — correctly. Fixed
with a narrow, self-policing exemption: conformance/incluster is exempt and asserted to
declare no route and no hostname-publishing Service, so the exemption can't rot into a fail-open.
Honest limits
Image pinning: the runner image doesn't exist, so this enforces the enforceable half —
under the conformance repo path and digest-pinned. Residual surface: anyone who can push
there. Tighten to the literal digest when the image is built.
Not wired: no runner image, no trigger kubeconfig, no CLUSTER_AVAILABLE. These identities
are inert until those exist, and #12 makes CLUSTER_AVAILABLE=true block rather than fake green.
Implements the credential model you chose: a **narrow trigger SA bounded by a fail-closed
Kyverno admission policy**.
### The escalation this closes
Kubernetes places **no `use` check** on a Job's `serviceAccountName`. Anyone who can
`create jobs` in ns `conformance` can create a Job running as `conformance-runner`
(nodes/patch, pods/exec, namespaces/patch) with an arbitrary command. A CI token with plain
`create jobs` **is** that privileged identity, however narrow its own Role looks. RBAC cannot
fix this — only constraining the *shape* of what the trigger may create.
### Verified with `kyverno apply` (pass 10, fail 6, **error 0**), not assumed
| Case | Result |
|---|---|
| `evil-command` — right SA, right image, arbitrary command | **DENIED** at `/containers/0/command/` ← *the* attack |
| `evil-image` | DENIED at `/image/` |
| `tag-not-digest` — mutable tag is republishable | DENIED at `/image/` |
| `host-namespaces` | DENIED at `/hostPID/` |
| `extra-volume` — the way out of a constrained pod | DENIED |
| `injected-args` | DENIED |
| `good-run` | **ADMITTED** |
The policy is `Enforce` + `failurePolicy: Fail` and deliberately does **not** use
`${KYVERNO_VALIDATION_ACTION}` — a boundary an overlay can downgrade to Audit is not a boundary.
It constrains *only* `conformance-trigger`, so a mistake here can't wedge the cluster.
### ⚠️ It also fixes a fail-open in the gate itself
**`kyverno test` does not fail when a policy cannot LOAD.** Every row becomes
`Skip / Invalid Policy` and it still prints *"N tests passed"* and exits 0. I hit this while
writing the policy — a schema error produced exactly that, and `ci/l0.sh` would have certified
**a security policy that does not load** as green.
Not specific to this policy: a typo in `restrict-host-namespaces` or `secret-provenance` would
silently disable it with the gate still green. `ci/l0.sh` now scans for `Invalid Policy`.
Proven: broken policy → `l0 rc=1`; restored → `rc=0`.
### c2-root-surface
The new Kustomization reconciles a tree that check doesn't scan, so it failed — correctly. Fixed
with a **narrow, self-policing exemption**: `conformance/incluster` is exempt *and* asserted to
declare no route and no hostname-publishing Service, so the exemption can't rot into a fail-open.
### Honest limits
- **Image pinning:** the runner image doesn't exist, so this enforces the enforceable half —
under the conformance repo path **and** digest-pinned. Residual surface: anyone who can push
there. Tighten to the literal digest when the image is built.
- **Not wired:** no runner image, no trigger kubeconfig, no `CLUSTER_AVAILABLE`. These identities
are inert until those exist, and #12 makes `CLUSTER_AVAILABLE=true` block rather than fake green.
Gate: **PASS, rc 0, 76s**.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Implements the credential model the operator chose for wiring L2/L3 into CI: a narrow
trigger SA bounded by a fail-closed Kyverno admission policy, rather than the
zero-credential GitOps-native alternative (plan section 4, primary model).
THE ESCALATION THIS CLOSES. Kubernetes places NO `use` check on a Job's
serviceAccountName. Any principal that can `create jobs` in ns conformance can create a Job
running as conformance-runner -- nodes/patch, pods/exec, namespaces/patch -- with an
arbitrary command. A CI token with plain `create jobs` IS that privileged identity no matter
how narrow its own Role looks. RBAC cannot fix this; only constraining the SHAPE of what the
trigger may create can.
Adds:
* SA/Role/RoleBinding conformance-trigger -- create+get+list+watch jobs, get pods and
pods/log, nothing else. Namespaced. No delete (a trigger that can delete can hide a
failed run), no exec, no secrets. Results come back as a tarball on stdout read via
pods/log, so no execution channel into the runner pod is ever granted.
* ClusterPolicy conformance-trigger-job-shape -- Enforce, failurePolicy: Fail. It does NOT
use ${KYVERNO_VALIDATION_ACTION}: a security boundary an overlay can downgrade to Audit
is not a boundary. It constrains ONLY conformance-trigger, so a mistake here cannot wedge
the rest of the cluster.
* Flux Kustomization `conformance` -> ./conformance/incluster, wait:false and non-gating
like apps-vms, so a test identity can never stall the platform or app tiers.
* A kyverno test with seven cases, six of them real escalation paths.
VERIFIED by `kyverno apply` (pass: 10, fail: 6, error: 0), not assumed:
evil-command DENIED at /spec/template/spec/containers/0/command/ <- THE attack
evil-image DENIED at .../image/
tag-not-digest DENIED at .../image/ (mutable tag is republishable)
host-namespaces DENIED at /spec/template/spec/hostPID/
extra-volume DENIED by the volumes foreach
injected-args DENIED by the args foreach
good-run ADMITTED
ALSO FIXES A FAIL-OPEN IN THE GATE ITSELF. `kyverno test` does NOT fail when a policy cannot
LOAD: every row becomes "Skip / Invalid Policy" and it still prints "N tests passed" and
exits 0. Measured while writing this policy -- a schema error (subjects in a separate
match.all entry from resources) produced exactly that, and ci/l0.sh would have certified a
security policy that does not load as green. This is not specific to this policy: a typo in
restrict-host-namespaces or secret-provenance would silently disable it with the gate still
green. ci/l0.sh now scans for "Invalid Policy" and fails. Proven: broken policy -> l0 rc=1,
restored -> rc=0.
IMAGE PINNING, HONEST LIMIT: the plan specifies pinning the exact runner image digest. That
image does not exist yet, so this enforces the enforceable half -- the image must be under
the conformance repository path AND digest-pinned, never a mutable tag. Residual surface:
anyone who can PUSH to that path could publish a different digest. Tighten to the literal
digest when the image is built; it is a one-line change to the pattern, reviewed in a PR,
which is where a change to a security boundary belongs.
NOT YET WIRED, deliberately: no runner image, no trigger kubeconfig, no CLUSTER_AVAILABLE.
The identities here are inert until those exist, and PR #12 makes CLUSTER_AVAILABLE=true
block rather than fake a green in the meantime.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements the credential model you chose: a narrow trigger SA bounded by a fail-closed
Kyverno admission policy.
The escalation this closes
Kubernetes places no
usecheck on a Job'sserviceAccountName. Anyone who cancreate jobsin nsconformancecan create a Job running asconformance-runner(nodes/patch, pods/exec, namespaces/patch) with an arbitrary command. A CI token with plain
create jobsis that privileged identity, however narrow its own Role looks. RBAC cannotfix this — only constraining the shape of what the trigger may create.
Verified with
kyverno apply(pass 10, fail 6, error 0), not assumedevil-command— right SA, right image, arbitrary command/containers/0/command/← the attackevil-image/image/tag-not-digest— mutable tag is republishable/image/host-namespaces/hostPID/extra-volume— the way out of a constrained podinjected-argsgood-runThe policy is
Enforce+failurePolicy: Failand deliberately does not use${KYVERNO_VALIDATION_ACTION}— a boundary an overlay can downgrade to Audit is not a boundary.It constrains only
conformance-trigger, so a mistake here can't wedge the cluster.⚠️ It also fixes a fail-open in the gate itself
kyverno testdoes not fail when a policy cannot LOAD. Every row becomesSkip / Invalid Policyand it still prints "N tests passed" and exits 0. I hit this whilewriting the policy — a schema error produced exactly that, and
ci/l0.shwould have certifieda security policy that does not load as green.
Not specific to this policy: a typo in
restrict-host-namespacesorsecret-provenancewouldsilently disable it with the gate still green.
ci/l0.shnow scans forInvalid Policy.Proven: broken policy →
l0 rc=1; restored →rc=0.c2-root-surface
The new Kustomization reconciles a tree that check doesn't scan, so it failed — correctly. Fixed
with a narrow, self-policing exemption:
conformance/inclusteris exempt and asserted todeclare no route and no hostname-publishing Service, so the exemption can't rot into a fail-open.
Honest limits
under the conformance repo path and digest-pinned. Residual surface: anyone who can push
there. Tighten to the literal digest when the image is built.
CLUSTER_AVAILABLE. These identitiesare inert until those exist, and #12 makes
CLUSTER_AVAILABLE=trueblock rather than fake green.Gate: PASS, rc 0, 76s.
Co-Authored-By: Claude Opus 5 (1M context) [email protected]
🤖 Generated with Claude Code
Implements the credential model the operator chose for wiring L2/L3 into CI: a narrow trigger SA bounded by a fail-closed Kyverno admission policy, rather than the zero-credential GitOps-native alternative (plan section 4, primary model). THE ESCALATION THIS CLOSES. Kubernetes places NO `use` check on a Job's serviceAccountName. Any principal that can `create jobs` in ns conformance can create a Job running as conformance-runner -- nodes/patch, pods/exec, namespaces/patch -- with an arbitrary command. A CI token with plain `create jobs` IS that privileged identity no matter how narrow its own Role looks. RBAC cannot fix this; only constraining the SHAPE of what the trigger may create can. Adds: * SA/Role/RoleBinding conformance-trigger -- create+get+list+watch jobs, get pods and pods/log, nothing else. Namespaced. No delete (a trigger that can delete can hide a failed run), no exec, no secrets. Results come back as a tarball on stdout read via pods/log, so no execution channel into the runner pod is ever granted. * ClusterPolicy conformance-trigger-job-shape -- Enforce, failurePolicy: Fail. It does NOT use ${KYVERNO_VALIDATION_ACTION}: a security boundary an overlay can downgrade to Audit is not a boundary. It constrains ONLY conformance-trigger, so a mistake here cannot wedge the rest of the cluster. * Flux Kustomization `conformance` -> ./conformance/incluster, wait:false and non-gating like apps-vms, so a test identity can never stall the platform or app tiers. * A kyverno test with seven cases, six of them real escalation paths. VERIFIED by `kyverno apply` (pass: 10, fail: 6, error: 0), not assumed: evil-command DENIED at /spec/template/spec/containers/0/command/ <- THE attack evil-image DENIED at .../image/ tag-not-digest DENIED at .../image/ (mutable tag is republishable) host-namespaces DENIED at /spec/template/spec/hostPID/ extra-volume DENIED by the volumes foreach injected-args DENIED by the args foreach good-run ADMITTED ALSO FIXES A FAIL-OPEN IN THE GATE ITSELF. `kyverno test` does NOT fail when a policy cannot LOAD: every row becomes "Skip / Invalid Policy" and it still prints "N tests passed" and exits 0. Measured while writing this policy -- a schema error (subjects in a separate match.all entry from resources) produced exactly that, and ci/l0.sh would have certified a security policy that does not load as green. This is not specific to this policy: a typo in restrict-host-namespaces or secret-provenance would silently disable it with the gate still green. ci/l0.sh now scans for "Invalid Policy" and fails. Proven: broken policy -> l0 rc=1, restored -> rc=0. IMAGE PINNING, HONEST LIMIT: the plan specifies pinning the exact runner image digest. That image does not exist yet, so this enforces the enforceable half -- the image must be under the conformance repository path AND digest-pinned, never a mutable tag. Residual surface: anyone who can PUSH to that path could publish a different digest. Tighten to the literal digest when the image is built; it is a one-line change to the pattern, reviewed in a PR, which is where a change to a security boundary belongs. NOT YET WIRED, deliberately: no runner image, no trigger kubeconfig, no CLUSTER_AVAILABLE. The identities here are inert until those exist, and PR #12 makes CLUSTER_AVAILABLE=true block rather than fake a green in the meantime. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>