Files
home-network-k3s-impl/decisions/2026-09-29-conformance-trigger-kubeconfig.md
jakman85andClaude Opus 5 ad5de2dba9
gate / gate (pull_request) Successful in 1m35s
gate / conformance (pull_request) Has been skipped
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) <[email protected]>
2026-09-29 22:51:14 -05:00

8.1 KiB

date, branch, title
date branch title
2026-09-29 feat/conformance-trigger-kubeconfig 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.