From ad5de2dba9fef9e9d0b4c916f7abbd6e7fc4ea29 Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Tue, 29 Sep 2026 22:45:04 -0500 Subject: [PATCH] docs(decisions): move PR #19's handoff out of the frozen DECISIONS-LOG.md This branch originally appended ~99 lines to the tail of DECISIONS-LOG.md. That tail is the hunk every concurrent branch collides on: measured against the live Gitea on 2026-09-29, four of the five open PRs were mergeable:false and DECISIONS-LOG.md was the ONLY conflicting path in all four. PR #19 was one of them. Nothing was wrong with the change; the log's storage format serialised work that was otherwise parallel. So the handoff now lives in its own file, decisions/2026-09-29-conformance-trigger-kubeconfig.md, in the one-entry-per-file layout PR #24 introduces (frontmatter date/branch/title, plus "What changed" / "What I verified" / "What I did NOT verify"). Concurrent branches then touch disjoint paths and there is nothing to merge. The branch was rebuilt on today's main with the DECISIONS-LOG.md change dropped entirely; infra/gitea/README.md and infra/gitea/conformance-trigger-kubeconfig.sh are byte-identical to the original commits. The original commit that touched ONLY DECISIONS-LOG.md -- the measurement that PR #19's CI has no approval-policy check -- was dropped as a commit and its substance folded into the new entry verbatim in meaning, including every UNVERIFIED it recorded. No content was upgraded. The script is still UNEXECUTED: nothing minted, no Gitea object written, no kubectl mutation, and the Gitea variables API shape remains documentation rather than measurement. Co-Authored-By: Claude Opus 5 (1M context) --- ...26-09-29-conformance-trigger-kubeconfig.md | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 decisions/2026-09-29-conformance-trigger-kubeconfig.md diff --git a/decisions/2026-09-29-conformance-trigger-kubeconfig.md b/decisions/2026-09-29-conformance-trigger-kubeconfig.md new file mode 100644 index 0000000..45a9671 --- /dev/null +++ b/decisions/2026-09-29-conformance-trigger-kubeconfig.md @@ -0,0 +1,125 @@ +--- +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.