Operator rule 2026-10-29: image creation, hosting and lifecycle belong to CI/CD, never to a workstation build/push. Nothing had ever been pushed to the Gitea registry. - .gitea/workflows/images.yaml + scripts/images.sh: PRs build+verify (no credential); main builds, verifies and pushes toolbox, agent-runner (FROM toolbox) and conformance-runner as sha-<commit> + main, reading the digest back from the registry. Only changed images build. - Each image's own build-image.sh does the build and in-image verification, now engine-neutral (DinD: no podman-only calls, probe over stdin not a bind mount). New toolbox build-image.sh. - Digest references carry `# renovate-digest:` markers; renovate.json5 gains a custom.regex manager for them. The worker Job template is deliberately untracked (it would loop). - renovate.yaml pins renovate-version 44.167.0: the action's default (39.264.0) rejects the current config's managerFilePatterns, so every run would have died at CONFIG_VALIDATION. - Push credential: REGISTRY_PUSH_TOKEN secret; the Actions token has no package scope in Gitea 1.24.3 (container.go Authenticate). Retention: infra/gitea/registry-retention.sh. Co-Authored-By: Claude Opus 5.5 <[email protected]>
8.5 KiB
In-cluster conformance Job (L2/L3)
STATUS NOTE (2026-09-29) — read before trusting the "Phase 0" framing below. This document speaks as though no cluster exists and the platform is scaffolding. A three-node k3s cluster has existed for weeks and Flux has been reconciling against it. Gitea is live, the Actions runner
incluster-dindis online, andmainis a protected branch. Where the text says "Phase 0", "stub", or "no cluster exists", read it as describing a live gap rather than an expected stage — most importantly, L2/L3 conformance still does not run in CI (vars.CLUSTER_AVAILABLEis unset), which is a real coverage hole, not a phase.This mismatch is not cosmetic. A month of "the gate enforces this" claims rested on documentation rather than measurement, and
mainwas unprotected with zero runners registered the whole time. Query the live API before asserting platform state.
Per ADR-0011 and docs/14 "Where each layer runs": L0/L1 run in CI without a cluster, but L2 behavioral and L3 fault-injection run as an in-cluster Chainsaw Job on the dev/test cluster that CI triggers and waits for. CI holds no cluster credentials — the Job's own scoped ServiceAccount does (R-SEC-02).
Files
rbac.yaml— theconformancenamespace, theconformance-runnerServiceAccount, and a least-privilege ClusterRole skeleton. Rules start empty and are added — each commented with the test/requirement that needs it — only as the matching fault-injection 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 clone init container delivers the suite at the commit under test; artifacts leave over stdout. Its header is the authority on the substitution tokens and the admission constraints it satisfies, and records theinitContainershole that was found and closed alongside it — read it before editing either file.Containerfile— theconformance-runnerimage:bash+python3+chainsaw+kubectl+git, non-root (numeric uid 65532) and working under a read-only root filesystem. This is what the Job actually needs; the stock chainsaw image carries none of it. Versions are--build-arg-injected frominfra/devcontainer/versions.env— the single source of truth — so the chainsaw/kubectl in the runner are the same pins the toolbox gate uses. Read its header before changing the Job'sargs:run.shruns the selected offline L0/L1 checks for any non-offline-only selection, and those need flux/promtool/kyverno/ kustomize, which this image deliberately omits. Use--layer l2/--layer l3.conformance-clone— the init container's only entrypoint, baked into the image at/usr/local/bin/conformance-clone(0755 root:root, so uid 65532 cannot replace it). 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 justargson/bin/bash -c. Clones the impl repo at the commit under test: the SHA must match^[0-9a-f]{40}$andrev-parse HEADis asserted equal to it afterwards, so an unrendered placeholder is a hard failure rather than a silent run of the branch tip. It takes no arguments, and the repository URL and clone target are compiled in —CONFORMANCE_REPO_URL/CONFORMANCE_SRCare hard-failed on a mismatch rather than honoured, because a URL taken from env would let the trigger choose which repository's code runs asconformance-runner. This branch's Job depends on that interface:CONFORMANCE_GIT_SHA,GITEA_USERNAME/GITEA_TOKEN, a clone target of/workspace/src(henceworkingDir: /workspace/src/conformance), andTMPDIR/HOMEunder/workspace.The other half landed with this branch. When
conformance-clonearrived, the admission policy pinned the main container'scommandbut never readinitContainers— an init container withcommand: ["/bin/sh","-c"]and an arbitrary script was ADMITTED and ran as the privilegedconformance-runnerSA, so this file was merely available rather than required. The rule that closes it istrigger-initcontainers-must-be-the-committed-clone-step: at most one init container, digest-pinned to the conformance-runner image,commandexactly/usr/local/bin/conformance-clone, and noargsat all.ephemeralContainersandenv/envFromwere the same shape of hole and are closed in the same policy.build-image.sh— builds the above and verifies the toolchain by running it inside the image as uid 65532 with--read-only. It never pushes, and neither does anyone from a workstation: CI (.gitea/workflows/images.yaml) runs this same script, and on a push tomainpublishes the verified image undersha-<commit>andmain. Renovate then movesCONFORMANCE_RUNNER_IMAGE_DIGESTinconformance/versions.envonto the registry-returned digest in a pull request (that file is a sensitive path, so the bump needs an administrator's/approve-override).--verify-cloneadditionally hasconformance-clonereally clone this repo at a real SHA and asserts the checkout matches (opt-in, because it needs the Gitea host reachable — the default verify stays offline so an air-gapped build is fully checked, not half-checked).
Flow
- CI (Gitea Actions) renders
chainsaw-job.yaml— two@@TOKEN@@substitutions, the runner image digest and the commit SHA — replacesargswith the invocation shape, andcreates the Job under the narrowconformance-triggeridentity (create/get jobs, get pods + pods/log — not a broad kubeconfig, and deliberately nopods/exec). - 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. - The init container runs
/usr/local/bin/conformance-clonewith 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 assertsHEADequals the requested SHA — the suite runs at the commit under test or not at all. The read-only Gitea credential reachesgitas env vars fromsecretKeyRef(secretconformance-git-readonly, keysusername/token), never as a volume: admission allows no volume butworkspaceandresults. - The runner executes
./run.shunderconformance-runner, writingreport.junit.xml+coverage/to theresultsvolume at/results(CONF_ARTIFACTS). - On exit — green, red or aborted —
run.shprints a delimited base64 tar of that tree to stdout, carryingbytes,linesandsha256in both the BEGIN and END markers. CI decodes it fromkubectl logsand must make all five completeness checks listed aboveconf_emit_artifacts_tgz()in../run.sh; a partial log then fails collection instead of decoding to plausible garbage.
Prerequisites before this Job can run at all
- Secret
conformance-git-readonlyin namespaceconformance, keysusernameandtoken: a read-only Gitea token for this repository. Never a write token or a push-capable deploy key. Does not exist — operator, out of GitOps (plan §3 item 6). - The
conformance-runnerimage (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/fsGrouppin it, so a different uid in the Containerfile means the Job will not start; conformance-cloneinstalled at/usr/local/bin/conformance-clone, mode 0755. The policy ruletrigger-initcontainers-must-be-the-committed-clone-steppins that exact path, so an image without it cannot run the clone step and no other path will be admitted.
- bash, git, python3, chainsaw and kubectl on
Phase 0
No cluster exists, so nothing here is applied yet. These manifests are reconciled by Flux
from Phase 1+ and first exercised when L2/L3 can run (T-3.0 onward). In Phase 0 the same
run.sh runs in CI's no-cluster stage and emits a skipped JUnit + the coverage report.