Operator decision on PR #81: no CI job may reach a privileged daemon. The act runner is untouched. - platform/buildkit (+ clusters/base/buildkit.yaml): moby/buildkit v0.34.1-rootless pinned by digest, uid 1000, not privileged; caps drop ALL + SETUID/SETGID, seccomp/AppArmor Unconfined, read-only rootfs (each measured under podman). cert-manager CA + server/client certs, ClusterIP :1234, default-deny with ingress only from the act-runner pod (DinD masquerades jobs to it). No Kyverno exception: every applied policy matches app-namespace=true only. - Each Containerfile names its shipped stage and ends in a `verify` stage holding every former `docker run` assertion (uid 65532, RUN --network=none, writable-path inventory standing in for the read-only rootfs). The old `&& echo` checks that could never fail are real assertions now. - images.sh drives buildkitd with a pinned buildx (sha256-checked) via bake; PRs build verify targets with agent-runner FROM the in-run toolbox (target: context); main verifies, then pushes. Fails loudly when the builder secrets are absent or buildkitd is unreachable. - infra/gitea/buildkit-client-secret.sh copies the client cert into Actions secrets. Co-Authored-By: Claude Opus 5.5 <[email protected]>
8.6 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: the Containerfile'sverifystage executes every tool and everyconformance-clonerefusal as uid 65532 with no network, at build time. It never pushes, and neither does anyone from a workstation: CI (.gitea/workflows/images.yaml) builds the same Containerfile andverifystage on rootless BuildKit (platform/buildkit/), 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.