--- date: 2026-09-29 branch: feat/conformance-trigger-kubeconfig title: The trigger credential script — it REFUSES, on purpose, and it is UNEXECUTED --- ## What changed Delivered `infra/gitea/conformance-trigger-kubeconfig.sh` — `docs/plans/l2-l3-conformance-ci-wiring.md` §3 item 6 — plus its section in `infra/gitea/README.md`. **The script is UNEXECUTED.** Minting a cluster credential and changing Gitea variables are operator actions; nothing was minted, no Gitea object was written, no `kubectl` mutation was made. Everything below is a claim about the *script*, not about the platform. *What it does.* Mints a token for `conformance/conformance-trigger` (TokenRequest), wraps **only that token** with the cluster CA and server into a kubeconfig, stores it as the Actions secret `CONFORMANCE_TRIGGER_KUBECONFIG` (legal name — **not** the reserved `GITEA_` prefix), and sets `vars.CLUSTER_AVAILABLE` only as a separately-requested, precondition-gated, TTY-confirmed final step. The token is never printed and never reaches argv: it goes into a 0600 file and into the PUT **body file**. The script refuses to run under `set -x`. *The design decision worth reviewing: TWO precondition classes.* - **mint-blocking (cluster-side):** SA/Role/RoleBinding present, and ClusterPolicy `conformance-trigger-job-shape` present + `Enforce` + `failurePolicy: Fail` + Ready. If any fails, **nothing is minted**. Rationale: Kubernetes puts no `use` check on a Job's `serviceAccountName`, so a `create jobs` token in that namespace IS the privileged `conformance-runner` identity unless the admission policy bounds it. RBAC cannot fix that (plan §4); minting before the boundary is live would hand CI the escalation the boundary exists to close. - **arm-blocking (repo-side):** digest-pinned `CONFORMANCE_RUNNER_IMAGE`, an init-container plus the admitted image in `chainsaw-job.yaml`, and `CONFORMANCE_TRIGGER_KUBECONFIG` referenced in `ci/conformance.sh` **code rather than a comment**. These block only the variable, because storing the secret early is harmless and is the plan's own order. **Measured in the tree at the time of writing: all three arm-blocking checks FAIL** (no `CONFORMANCE_RUNNER_IMAGE` in `conformance/versions.env`; `chainsaw-job.yaml` is still the skeleton with an empty `workspace` emptyDir and the stock chainsaw image; `ci/conformance.sh` names the trigger kubeconfig only in its TODO comment). So on a live run today the script would provision the secret and then **refuse to arm `CLUSTER_AVAILABLE`**, naming each missing item — which is the intended behaviour, not a defect. §3 items 1, 2 and 4 are the work that unblocks it. *Both writes are verified the way `set-ci-token.sh` learned to.* `curl -sS` exits 0 on an HTTP error, so status alone proves nothing: each write checks `%{http_code}` **and** reads the object back. For the secret that is presence-by-name only (Gitea never returns secret values) and the script says so rather than implying the content was checked; for the variable it asserts the value reads back as exactly `true`. *Noted, not fixed at the time (since fixed on this branch).* `infra/gitea/README.md`'s detector table still named `GITEA_SETTINGS_TOKEN` — the illegal name PR #15 replaced with `SETTINGS_API_TOKEN`. The second commit on this branch corrects the README's table and its closing "remaining step" line, and records what `ci/gitea-settings.sh` actually does (`TOKEN="${SETTINGS_API_TOKEN:-${GITEA_SETTINGS_TOKEN:-}}"` — the old name survives only as a local-run environment variable, never as CI wiring, because the secret that would feed it cannot be created at all). Documentation only, no behaviour change. Still NOT fixed, because it is outside this PR's diff: `ci/gitea-settings.sh:45` instructs the reader to create a repo secret `GITEA_SETTINGS_TOKEN`, which is impossible. *Rework, 2026-09-29.* This branch originally appended ~99 lines to the tail of `DECISIONS-LOG.md`. That is the hunk every concurrent branch collided on and the sole reason PR #19 was `mergeable: false`. The branch was rebuilt on today's `main` with the `DECISIONS-LOG.md` change dropped entirely, and this file is that handoff moved into `decisions/`. The changes to `infra/gitea/**` are byte-identical to the original three commits; the commit that contained only the log append was dropped and its substance folded in below. ## What I verified *Of the script, before it stores anything:* it PROVES the credential narrow — `auth whoami` must return `system:serviceaccount:conformance:conformance-trigger`; a real `get jobs` round-trip must succeed; then an `auth can-i` matrix asserts BOTH halves — granted (`create/get jobs`, `get pods`, `get pods/log`) and **denied** (`delete jobs`, `create pods/exec`, `get secrets`, `create jobs` in `default`, `get nodes`, `'*' '*' --all-namespaces`). A mismatch aborts before anything is stored, and the message says explicitly **not** to widen `conformance/incluster/rbac.yaml` to make it pass. That is a property of the code as read, not of an execution. *Of PR #19's CI, measured rather than assumed — and it contradicts the brief.* The brief for this work stated this PR's CI would FAIL `approval-policy` because `infra/gitea/**` is security-relevant. Measured on the PR head commit's `/statuses`: gate / gate (pull_request) success Successful in 1m34s gate / conformance (pull_request) pending Blocked by required conditions `.gitea/workflows/` on `main` contains only `gate.yml` and the dormant `merge-gate.yaml`. **No path-scoped approval policy exists**, so nothing machine-enforces "security-relevant paths need a human"; that rule rests entirely on `required_approvals: 1` plus a reviewer actually reading the diff. Another instance of the CLAUDE.md §2b failure mode — a control believed to exist because a brief said so. If the policy is intended, it is missing and needs its own change. And per this repo's own rule, that green `gate` tick is **not** evidence this script works: the gate never executes `infra/gitea/**`, and the L2/L3 stage skipped. *Gate on the reworked branch (pinned toolbox, from the agent worktree, `DEV_KUBECONFIG=/dev/null` plus the `ENGINE` bind-mount shim per CLAUDE.md §3):* `MERGE GATE: UNDETERMINED`, **rc 3**. Diff base `origin/main @ c59fe9f923c4` ... this branch's head; touched requirement resolved as `R-ENV-01`. `spec-immutability`, `decisions-log`, `lint`, `l0`, `render-golden` and `l1` all green; the gate decision names exactly one stage that did not run — `gitea-settings` (`SETTINGS_API_TOKEN` is a credential this agent does not hold, unwired repo-wide). The `conformance` stage also reported SKIPPED (`CLUSTER_AVAILABLE!=true`, Phase 0). The new `decisions-log` stage — merged to `main` as PR #24 after this rework began — validated this very file: "archive frozen, 2 entries well-formed", with `DECISIONS-LOG.md` unchanged and matching `decisions/ARCHIVE.sha256`. rc 3 is UNDETERMINED, not green, and is not reported as green here. ## What I did NOT verify **The script has never executed.** Not one line of it has run against Gitea or against the cluster. Only `bash -n` was run (clean); `shellcheck` is not on the host and there is no shellcheck stage in the gate. - The Gitea variables API shape (POST-creates / PUT-updates, body field `value` not `data`) is from **documentation, not measurement**. The script tries POST then falls back to PUT, prints the response body on failure, and treats the read-back as the authority. - The Kyverno readiness field (`.status.ready` vs. a Ready condition) is handled both ways and treats "neither present" as a block rather than assuming ready — but which form this cluster actually reports is **unmeasured**. - Whether the kubeconfig's embedded server URL is reachable from the runner pod **cannot be known from here** — hence `--server`, and a hard refusal on a loopback address. - No credential was minted, no Actions secret was created, `vars.CLUSTER_AVAILABLE` was not set, and no `can-i` matrix was ever run against a real token. The narrowness argument above is read from the RBAC and the script, not observed. - The gate proves only its L0/L1 offline subset. L2/L3 conformance did not run, so nothing here is evidence about runtime behaviour.