feat(sec): bake conformance-clone so the init container has no injection surface

Closes half of the admission gap a sibling agent found in PR #13's policy: it pins
the MAIN container's `command` but never reads `initContainers`, so a Job from
`conformance-trigger` can carry an init container with `command: ["/bin/sh","-c"]`
and an arbitrary script, running as the privileged `conformance-runner` SA.
Reproduced by the policy's author: pass 3, fail 0 on a privileged alpine:latest
init container running `kubectl get secrets -A`.

The image now carries /usr/local/bin/conformance-clone, 0755 root:root, so uid
65532 cannot replace the one executable the policy will come to trust. Clone logic
taken from PR #20's `clone-suite-at-commit`, with both load-bearing properties
preserved: the SHA must match ^[0-9a-f]{40}$, and `rev-parse HEAD` is ASSERTED
equal to it after checkout, so an unrendered placeholder is a hard failure instead
of a silent run of the branch tip. No arguments are accepted.

DEVIATION FROM THE BRIEF, DELIBERATE: the repo URL and clone target are COMPILED
IN rather than read from the environment. I was asked to take SHA, repo URL and
target dir from env. The policy contains no `env`/`valueFrom`/`secretKeyRef` clause
at all -- so trusting CONFORMANCE_REPO_URL would move the injection one field over:
the trigger points it at a repo it controls, this fetches that, and ./run.sh from
the attacker's tree runs under the privileged SA. "No args" would not have stopped
it. The env vars are still read so the Job template stays self-documenting, but a
value disagreeing with the baked literal is a hard failure, never a redirect. The
SHA is the only genuinely trusted per-run input.

THIS IS HALF A FIX. What makes the fixed path REQUIRED rather than merely available
is a 4th policy rule demanding command: ["/usr/local/bin/conformance-clone"] with
no args on every initContainer. That edits the security boundary and belongs in its
own reviewed PR -- it is NOT here. Until it lands the hole is open, and the README,
the Containerfile and the script header all say so.

VERIFIED by running it (uid 65532, --read-only): root:root 755 and confirmed not
writable by the runner; seven negative cases each asserted to exit non-zero
(arguments, missing SHA, a branch name, an unrendered @@placeholder@@, HOME off the
workspace volume, a REDIRECTED repo URL, a clone target off the workspace volume);
and the positive case -- a real clone of this repo at a real 40-hex SHA with
rev-parse HEAD proven equal to it.

Two bugs of mine that only running it could find, both fixed: the workspace-path
guard matched /workspace/* and so rejected the image's own HOME=/workspace; and the
negative checks were pipelines, which under pipefail aborted the verify at the
first expected failure and silently skipped the other six. A verify that stops
early looks exactly like a verify that passed.

The anonymous-clone fallback is deliberate and logged: read access to this repo
needs no credential today, the integrity guarantee is the SHA assertion rather than
the credential, and requiring the Secret would make the script untestable without
one.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
jakman85andClaude Opus 5 committed 2026-09-29 22:49:34 -05:00
1 parent 529556d7c7
commit ab9ba32a76
5 files changed
+257

No files matched your search

+24
View File
@@ -17,6 +17,10 @@
# run.sh's `rev-parse --short HEAD` for the run id
# tar/gzip/coreutils the `=== ARTIFACTS-TGZ-BASE64 ===` results channel (§3 item 3)
#
# It also carries `/usr/local/bin/conformance-clone` (0755 root:root) — the init container's
# ONLY entrypoint, a fixed path with no argument surface. That exists to close an admission
# hole, not for convenience: see the COPY below and the script's own header.
#
# VERSIONS COME FROM infra/devcontainer/versions.env — NOT from new pins invented here.
# That file is the single source of truth for CHAINSAW_VERSION / KUBECTL_VERSION /
# FEDORA_VERSION, and build-image.sh sources it and passes them in as --build-arg. The
@@ -124,6 +128,25 @@ RUN set -eux; \
COPY --from=builder /out/kubectl /out/chainsaw /usr/local/bin/
# The init container's ONLY entrypoint. Copied as a file with a fixed path and NO argument
# surface, because the admission policy pins the main container's `command` but never reads
# `initContainers` — so an init container with `command: ["/bin/sh","-c"]` and an arbitrary
# script is currently ADMITTED and runs as the privileged `conformance-runner` SA (reproduced
# by the policy's author; pass 3, fail 0). A 4th policy rule requiring
# `command: ["/usr/local/bin/conformance-clone"]` with no `args` is what makes this file
# REQUIRED rather than merely available; that rule belongs in its own reviewed PR against the
# boundary, not here. See the script's header for the env-injection half of the same hole.
#
# 0755 root:root: the container runs as 65532 and MUST NOT be able to replace the one
# executable the policy will trust. Ownership is asserted below rather than assumed.
COPY conformance-clone /usr/local/bin/conformance-clone
RUN set -eux; \
chown root:root /usr/local/bin/conformance-clone; \
chmod 0755 /usr/local/bin/conformance-clone; \
bash -n /usr/local/bin/conformance-clone; \
/usr/local/bin/conformance-clone --anything 2>&1 | grep -q 'takes no arguments'; \
test "$(stat -c '%U:%G %a' /usr/local/bin/conformance-clone)" = 'root:root 755'
# The nonroot account is written directly rather than pulling in shadow-utils to run useradd
# once. HOME is /workspace because at runtime that emptyDir is the only writable path.
RUN set -eux; \
@@ -154,6 +177,7 @@ RUN set -eux; \
echo "bash=$(bash --version | head -1)"; \
echo "python3=$(python3 --version)"; \
echo "git=$(git --version)"; \
echo "conformance-clone=$(sha256sum /usr/local/bin/conformance-clone | cut -d' ' -f1)"; \
} > /etc/conformance-runner-versions; \
chmod 0444 /etc/conformance-runner-versions
+16
View File
@@ -39,10 +39,26 @@ credentials — the Job's own scoped ServiceAccount does (R-SEC-02).
uses. Read its header before changing the Job's `args`: `run.sh` runs 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).
Clones the impl repo at the commit under test: the SHA must match `^[0-9a-f]{40}$` and
`rev-parse HEAD` is **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** — see its header for why the env vars
are read but may not override them.
> **This file is half a fix.** The admission policy pins the main container's `command` but
> **never reads `initContainers`**, so an init container with `command: ["/bin/sh","-c"]` and
> an arbitrary script is currently ADMITTED and runs as the privileged `conformance-runner`
> SA. What makes this file *required* rather than merely available is a 4th policy rule
> demanding `command: ["/usr/local/bin/conformance-clone"]` with no `args`. That rule belongs
> in its own reviewed PR against the boundary and is **not** in this branch.
- `build-image.sh` — builds the above, **verifies the toolchain by running it** inside the
image as uid 65532 with `--read-only`, and prints the digest. It **never pushes**: it
prints the exact `podman push --digestfile …` command for the operator instead. See
`conformance/versions.env` `CONFORMANCE_RUNNER_IMAGE` for the post-push pin.
`--verify-clone` additionally has `conformance-clone` really 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
1. CI (Gitea Actions, delivered in T-6.1b) creates a Job from `chainsaw-job.yaml` (via a
+73
View File
@@ -5,6 +5,10 @@
#
# ./build-image.sh # build + verify + print the digest and push command
# ./build-image.sh --no-verify # build only (skip the in-image tool proof)
# ./build-image.sh --verify-clone # ALSO really clone this repo with conformance-clone.
# # Opt-in because it needs the Gitea host reachable; the
# # default verify is offline so an air-gapped build is
# # still fully checked rather than half-checked.
# ENGINE=docker ./build-image.sh # build with docker instead of podman
#
# THIS SCRIPT NEVER PUSHES. Publishing is an operator action: the registry credential is
@@ -30,9 +34,11 @@ REPO_ROOT="$(cd "${HERE}/../.." && pwd)"
ENGINE="${ENGINE:-podman}"
VERIFY=1
VERIFY_CLONE=0
for arg in "$@"; do
case "$arg" in
--no-verify) VERIFY=0 ;;
--verify-clone) VERIFY_CLONE=1 ;;
-h|--help) sed -n '2,/^####/p' "${BASH_SOURCE[0]}" | sed '$d' | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown argument: $arg" >&2; exit 2 ;;
esac
@@ -100,6 +106,44 @@ if [ "${VERIFY}" -eq 1 ]; then
echo "--- kubectl ---"; kubectl version --client
echo "--- chainsaw ---"; chainsaw version
echo "--- baked versions ---"; cat /etc/conformance-runner-versions
echo "--- conformance-clone: fixed path, root-owned, NOT writable by the runner ---"
stat -c "%n %U:%G %a" /usr/local/bin/conformance-clone
if [ -w /usr/local/bin/conformance-clone ]; then
echo "UNEXPECTED: uid $(id -u) can write the one executable the policy will trust"; exit 1
fi
echo "confirmed: not writable by uid $(id -u)"
# Each of these MUST fail. Assert the non-zero exit as well as the message: a check
# that only greps stderr would pass a build where the guard had stopped rejecting.
# (`|| true` inside the capture keeps `set -e`/`pipefail` from aborting on the
# expected failure — without it the verify dies at the first negative case.)
expect_fail() {
local label="$1"; shift
local out rc=0
out="$(env "$@" 2>&1)" || rc=$?
echo "--- ${label} ---"
printf "%s\n" "${out}" | head -2
if [ "${rc}" -eq 0 ]; then
echo "UNEXPECTED: that should have failed, but it exited 0"; exit 1
fi
echo "(rejected, exit ${rc})"
}
expect_fail "conformance-clone rejects arguments (that is where an injection would live)" \
conformance-clone --layer l2
expect_fail "conformance-clone rejects a missing SHA" \
conformance-clone
expect_fail "conformance-clone rejects a branch name instead of a SHA (no silent run of the tip)" \
CONFORMANCE_GIT_SHA=main conformance-clone
expect_fail "conformance-clone rejects an UNRENDERED placeholder" \
CONFORMANCE_GIT_SHA=@@CONFORMANCE_GIT_SHA@@ conformance-clone
expect_fail "conformance-clone rejects a HOME off the workspace volume" \
CONFORMANCE_GIT_SHA=0000000000000000000000000000000000000000 \
HOME=/etc conformance-clone
expect_fail "conformance-clone refuses a REDIRECTED repository URL (the env-injection hole)" \
CONFORMANCE_GIT_SHA=0000000000000000000000000000000000000000 \
CONFORMANCE_REPO_URL=https://evil.invalid/x.git conformance-clone
expect_fail "conformance-clone refuses a clone target off the workspace volume" \
CONFORMANCE_GIT_SHA=0000000000000000000000000000000000000000 \
CONFORMANCE_SRC=/etc/src conformance-clone
echo "--- rootfs is read-only (expected: a failure) ---"
if touch /should-fail 2>/dev/null; then echo "UNEXPECTED: rootfs is writable"; exit 1; fi
echo "confirmed: / is read-only"
@@ -111,6 +155,35 @@ if [ "${VERIFY}" -eq 1 ]; then
log "Toolchain verified"
fi
# --- verify the clone helper against the REAL repository (opt-in: needs network) ----
# The negative cases above prove conformance-clone refuses bad input. This proves the
# positive case: it lands the EXACT commit asked for and can prove it did. Without this, the
# rev-parse assertion is only an unexercised line of shell.
if [ "${VERIFY_CLONE}" -eq 1 ]; then
log "Verifying conformance-clone against the real repository"
SHA="$(git -C "${REPO_ROOT}" rev-parse HEAD)"
info "requesting commit ${SHA}"
"${ENGINE}" run --rm \
--user 65532:65532 \
--read-only \
--tmpfs /workspace:rw,mode=1777 \
--network host \
--entrypoint "" \
-e "CONFORMANCE_GIT_SHA=${SHA}" \
"${IMAGE_REF}" \
/bin/bash -euo pipefail -c '
conformance-clone
echo "--- assert rev-parse HEAD == the requested SHA ---"
got="$(git -C /workspace/src rev-parse HEAD)"
echo "requested: ${CONFORMANCE_GIT_SHA}"
echo "HEAD: ${got}"
[ "${got}" = "${CONFORMANCE_GIT_SHA}" ] || { echo "MISMATCH"; exit 1; }
echo "confirmed: the working tree is at the requested commit"
test -x /workspace/src/conformance/run.sh && echo "confirmed: run.sh present and executable"
'
log "conformance-clone verified against the real repository"
fi
# --- digest ------------------------------------------------------------------
DIGEST="$("${ENGINE}" image inspect --format '{{.Digest}}' "${IMAGE_REF}")"
IMAGE_ID="$("${ENGINE}" image inspect --format '{{.Id}}' "${IMAGE_REF}")"
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
###############################################################################
# conformance-clone — fetch the impl repo at the exact commit under test.
#
# Baked into the conformance-runner image at /usr/local/bin/conformance-clone, mode 0755
# root:root, so the running container (uid 65532) cannot replace it. The Job's init
# container runs it as `command: ["/usr/local/bin/conformance-clone"]` with NO args.
#
# WHY IT IS A FILE IN THE IMAGE AND NOT AN INLINE SCRIPT IN THE JOB
# -----------------------------------------------------------------
# platform/kyverno/admission/conformance-trigger-job-shape.yaml pins the MAIN container's
# `command` to ["./run.sh"] and constrains its `args` — but it never reads `initContainers`.
# So a Job from `conformance-trigger` could carry an init container with
# `command: ["/bin/sh","-c"]` and an arbitrary script, running as the privileged
# `conformance-runner` SA. Reproduced by the policy's author: such a Job is ADMITTED
# (pass 3, fail 0).
#
# The fix has two halves and NEITHER works alone:
# 1. this file — a fixed path with fixed behaviour and no argument surface, so there is
# nothing for a caller to inject into; and
# 2. a 4th policy rule requiring every initContainer's `command` to be exactly
# ["/usr/local/bin/conformance-clone"] with no `args`. That rule lives in the policy
# and is NOT this branch's to write — a security boundary changes in its own reviewed PR.
# Without (2) this file is merely available, not required.
#
# THE ENV SURFACE IS THE OTHER HALF OF THE SAME HOLE — READ THIS BEFORE "SIMPLIFYING"
# -----------------------------------------------------------------------------------
# The admission policy says NOTHING about `env`. Verified: no `env`, `valueFrom` or
# `secretKeyRef` clause appears in it. So a script that took its repository URL from the
# environment would just move the injection one field over — the trigger sets
# CONFORMANCE_REPO_URL to a repo it controls, this clones it, and `./run.sh` from THAT repo
# executes under the privileged runner SA. Identical shape to the initContainers gap, and
# "no args" would not have stopped it.
#
# Therefore the repository URL and the clone target are COMPILED IN below. The matching env
# vars are still read, so the Job template can keep declaring them and stay self-documenting,
# but a value that DISAGREES with the baked literal is a hard failure, never a redirect.
# The only per-run input that is genuinely trusted is the commit SHA, and it must be 40 hex
# characters of a commit that the baked repository actually contains.
#
# Inputs (environment; no arguments are accepted):
# CONFORMANCE_GIT_SHA REQUIRED. 40 lowercase hex. The commit under test.
# CONFORMANCE_REPO_URL optional. Must equal the baked URL if set.
# CONFORMANCE_SRC optional. Must equal the baked target if set.
# GITEA_USERNAME optional, from Secret `conformance-git-readonly`.
# GITEA_TOKEN optional, from Secret `conformance-git-readonly`.
# TMPDIR / HOME optional; must be under /workspace. Default to /workspace/{tmp,home}.
#
# Runs under: uid 65532, readOnlyRootFilesystem, only the `workspace` and `results` volumes.
# Everything it writes therefore lives under /workspace. There is no writable /tmp.
###############################################################################
set -euo pipefail
# --- COMPILED-IN, NOT CONFIGURABLE (see the header) --------------------------
readonly BAKED_REPO_URL="https://gitea.home.keithjackson.dev/Home-Network/home-network-k3s-impl.git"
readonly BAKED_SRC="/workspace/src"
die() { printf 'FATAL: %s\n' "$*" >&2; exit 1; }
log() { printf '==> %s\n' "$*"; }
[ "$#" -eq 0 ] || die "takes no arguments (got $#: $*). Arguments are where an injection would live."
# --- the one genuinely trusted input ----------------------------------------
# A bad SHA must never degrade into "ran whatever the branch tip was": an unrendered
# @@CONFORMANCE_GIT_SHA@@ placeholder has to be a hard failure, not a silent run of main.
: "${CONFORMANCE_GIT_SHA:?CONFORMANCE_GIT_SHA is not set; the CI trigger did not render it}"
printf '%s' "${CONFORMANCE_GIT_SHA}" | grep -Eq '^[0-9a-f]{40}$' \
|| die "CONFORMANCE_GIT_SHA is not a 40-hex commit id: '${CONFORMANCE_GIT_SHA}'
The CI trigger did not substitute it, or substituted junk."
# --- env that may agree with the baked values, but may not override them -----
REPO_URL="${CONFORMANCE_REPO_URL:-${BAKED_REPO_URL}}"
[ "${REPO_URL}" = "${BAKED_REPO_URL}" ] || die "CONFORMANCE_REPO_URL does not match the image's baked repository.
got: ${REPO_URL}
baked: ${BAKED_REPO_URL}
Refusing. A caller that can choose the repository can choose the code that runs."
SRC="${CONFORMANCE_SRC:-${BAKED_SRC}}"
[ "${SRC}" = "${BAKED_SRC}" ] || die "CONFORMANCE_SRC does not match the image's baked clone target.
got: ${SRC}
baked: ${BAKED_SRC}
Refusing."
# Scratch paths must stay on the workspace volume; the root filesystem is read-only.
# `/workspace` ITSELF is allowed, not just subdirectories: the image's own ENV sets
# HOME=/workspace, and an earlier version of this guard rejected exactly that — it matched
# only `/workspace/*` and so refused its own default. Caught by running it, not by reading it.
: "${TMPDIR:=/workspace/tmp}"
: "${HOME:=/workspace/home}"
on_workspace() { case "$1" in /workspace|/workspace/*) return 0 ;; *) return 1 ;; esac; }
on_workspace "${TMPDIR}" || die "TMPDIR must be /workspace or under it (got '${TMPDIR}')"
on_workspace "${HOME}" || die "HOME must be /workspace or under it (got '${HOME}')"
export TMPDIR HOME
mkdir -p "${TMPDIR}" "${HOME}"
# --- credential (optional) ---------------------------------------------------
# git's shell credential helper is invoked as `sh -c '<value> get'`, so it reads $1 and
# answers from the environment. The token therefore never appears in argv and never on disk.
#
# ANONYMOUS FALLBACK, DELIBERATE AND ANNOUNCED. Read access to this repository currently
# needs no credential (measured: `ls-remote` succeeds unauthenticated from inside this
# image), so requiring the Secret would fail runs that would otherwise work — and would make
# this script untestable without one. It is not a privilege downgrade: the integrity
# guarantee here is the SHA assertion below, not the credential. Which mode was used is
# logged, so a run that silently lost its Secret is visible in the log rather than inferred.
CRED=''
if [ -n "${GITEA_USERNAME:-}" ] && [ -n "${GITEA_TOKEN:-}" ]; then
CRED='!f() { if [ "$1" = get ]; then printf "username=%s\npassword=%s\n" "$GITEA_USERNAME" "$GITEA_TOKEN"; fi; }; f'
log "authenticating as '${GITEA_USERNAME}' (token from the environment, never argv)"
else
log "no GITEA_USERNAME/GITEA_TOKEN in the environment — cloning ANONYMOUSLY"
fi
git_c() {
if [ -n "${CRED}" ]; then git -c credential.helper="${CRED}" "$@"; else git "$@"; fi
}
# --- clone, land the exact commit, and PROVE it ------------------------------
log "shallow clone ${REPO_URL} -> ${SRC}"
git_c clone --depth 1 --no-tags --single-branch "${REPO_URL}" "${SRC}"
if ! git -C "${SRC}" cat-file -e "${CONFORMANCE_GIT_SHA}^{commit}" 2>/dev/null; then
log "${CONFORMANCE_GIT_SHA} is not in the shallow tip; fetching that commit"
git_c -C "${SRC}" fetch --depth 1 --no-tags origin "${CONFORMANCE_GIT_SHA}"
fi
git -C "${SRC}" checkout --detach "${CONFORMANCE_GIT_SHA}"
# The assertion is the point of the whole script: everything above is best-effort plumbing,
# this is what makes "the suite ran at the commit under test" a fact rather than a hope.
head="$(git -C "${SRC}" rev-parse HEAD)"
[ "${head}" = "${CONFORMANCE_GIT_SHA}" ] \
|| die "checked out ${head}, expected ${CONFORMANCE_GIT_SHA}."
test -x "${SRC}/conformance/run.sh" \
|| die "no executable conformance/run.sh at ${head}."
log "suite ready at ${head}"
+6
View File
@@ -28,6 +28,12 @@ PYTHON_MIN="3.9"
# This — not CHAINSAW_IMAGE — is what the L2/L3 Job runs (bash + python3 + chainsaw +
# kubectl + git). Closes docs/plans/l2-l3-conformance-ci-wiring.md §3 item 1.
#
# It ALSO carries /usr/local/bin/conformance-clone (0755 root:root, no argument surface) as the
# init container's only entrypoint. That is a security fixture, not a convenience: the
# admission policy never reads `initContainers`, so an init container with an inline shell
# script is currently admitted with the privileged runner SA. The policy rule that makes the
# fixed path REQUIRED is not in this branch — see conformance/incluster/README.md.
#
# THE REPOSITORY PATH IS NOT A PREFERENCE. platform/kyverno/admission/
# conformance-trigger-job-shape.yaml is a fail-closed Enforce ClusterPolicy (R-SEC-02) that
# ADMITS a conformance-trigger Job only when its image matches