Files
home-network-k3s-impl/infra/agent/probe-validation.sh
jakman85andClaude Opus 5.5 f65c12b395 feat(images): build on rootless BuildKit over mTLS; verification moves into Containerfile verify stages
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]>
2026-10-29 23:59:55 -05:00

133 lines
6.5 KiB
Bash
Executable File

#!/usr/bin/env bash
###############################################################################
# infra/agent/probe-validation.sh — prove pm-worker REFUSES hostile input.
#
# ./probe-validation.sh # from anywhere; needs only bash
#
# WHY THIS EXISTS. pm-worker's `command` is pinned by admission with no args, so everything a
# caller could influence arrives in the ENVIRONMENT — and the guards at the top of that
# script are what stand between an injected variable and a clone from the wrong host, a push
# to the wrong ref, or a session with the wrong credential. Guards that nothing exercises are
# a claim, not a control. Each case below asserts BOTH a non-zero exit AND the specific
# message, because a check that only grepped stderr would pass a build where the guard had
# stopped rejecting and something else had started failing instead.
#
# Every case must die BEFORE the script reaches any network call: the probe runs with a
# scrubbed environment and fake credentials, and if a guard ever stopped firing, the case
# would fail here rather than by really cloning something.
#
# Run offline, no cluster, no Gitea, no model. The Containerfile's `verify` stage runs it inside
# the built image as well (as uid 65532, no network), where the interpreter and the script are
# the ones that will actually run in the cluster — locally via build-image.sh and in CI.
###############################################################################
set -uo pipefail
# `:-$0` keeps this safe if it is ever fed over stdin (`bash -s`), where BASH_SOURCE is empty
# and `set -u` would abort on it.
HERE="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
WORKER="${PM_WORKER_PATH:-${HERE}/pm-worker}"
[ -r "$WORKER" ] || { echo "cannot read ${WORKER}" >&2; exit 2; }
# A complete, VALID assignment. Each case below overrides exactly one thing, so a failure
# names the guard rather than a pile of missing variables.
BASE=(
PM_TASK_MODE=implement
PM_ISSUE_NUMBER=42
PM_BRANCH=feat/some-real-slug
PM_BASE_BRANCH=main
PM_GITEA_URL=http://gitea.gitea.svc.cluster.local:3000
PM_GITEA_OWNER=Home-Network
PM_GITEA_REPO=home-network-k3s-impl
PM_GITEA_USER=pm-agent
GITEA_TOKEN=not-a-real-token
CLAUDE_CODE_OAUTH_TOKEN=not-a-real-token
)
PASS=0
FAIL=0
# `env -i` so nothing from the invoking shell leaks in and accidentally satisfies a guard.
refuses() {
local label="$1" want="$2"; shift 2
local out rc=0
out="$(env -i PATH="$PATH" HOME=/tmp "${BASE[@]}" "$@" bash "$WORKER" 2>&1)" || rc=$?
if [ "$rc" -eq 0 ]; then
printf ' FAIL %s — exited 0; it should have been refused\n' "$label"
FAIL=$((FAIL + 1))
return
fi
if printf '%s' "$out" | grep -qF "$want"; then
printf ' ok %s (exit %s)\n' "$label" "$rc"
PASS=$((PASS + 1))
else
printf ' FAIL %s — refused (exit %s) but not for the expected reason %q:\n' "$label" "$rc" "$want"
printf '%s\n' "$out" | tail -3 | sed 's/^/ /'
FAIL=$((FAIL + 1))
fi
}
echo "pm-worker input-validation probe ($WORKER)"
# An argument on a pinned command is caller-supplied code. There is no argument surface.
out="$(env -i PATH="$PATH" HOME=/tmp "${BASE[@]}" bash "$WORKER" --layer l2 2>&1)" || rc=$?
if printf '%s' "$out" | grep -qF "takes no arguments"; then
echo " ok arguments are refused (exit ${rc:-?})"; PASS=$((PASS + 1))
else
echo " FAIL arguments are refused — got: $(printf '%s' "$out" | tail -1)"; FAIL=$((FAIL + 1))
fi
# --- the ref it may create and push ---------------------------------------
refuses "branch 'main' (CLAUDE.md §1)" "does not match" PM_BRANCH=main
refuses "branch with a wrong prefix" "does not match" PM_BRANCH=feature/x
refuses "branch with a shell metacharacter" "does not match" 'PM_BRANCH=feat/x;id'
refuses "branch with path traversal" "does not match" 'PM_BRANCH=../../etc/passwd'
refuses "branch too short for the slug rule" "does not match" PM_BRANCH=fix/ab
refuses "branch equal to the base branch" "must not be the base branch" \
PM_BRANCH=feat/same-slug PM_BASE_BRANCH=feat/same-slug
# --- where code comes FROM and goes TO -------------------------------------
# The namespace's egress policy permits tcp/443 to the world (the model needs it), so an
# injected host would be REACHABLE. These are the cases that matter most.
refuses "a redirected git host" "is not the allowed in-cluster git host" \
PM_GITEA_URL=https://evil.invalid
refuses "a real, reachable, WRONG git host" "is not the allowed in-cluster git host" \
PM_GITEA_URL=https://github.com
refuses "the right host on a smuggled path" "is not the allowed in-cluster git host" \
PM_GITEA_URL=http://gitea.gitea.svc.cluster.local:3000/../..
# This agent is IN the cluster, so it reaches Gitea by its cluster-local service name. The
# EXTERNAL hostname is refused too — it used to be a second accepted value, and as an
# accepted value it widened this allowlist to a host reachable over the `world:443` egress
# rule, which is the injection shape the allowlist exists to refuse.
refuses "this Gitea's EXTERNAL hostname" "is not the allowed in-cluster git host" \
PM_GITEA_URL=https://gitea.home.keithjackson.dev
refuses "the cluster-local name over https" "is not the allowed in-cluster git host" \
PM_GITEA_URL=https://gitea.gitea.svc.cluster.local:3000
refuses "the cluster-local name, wrong port" "is not the allowed in-cluster git host" \
PM_GITEA_URL=http://gitea.gitea.svc.cluster.local:8080
refuses "a malformed owner" "PM_GITEA_OWNER is malformed" \
'PM_GITEA_OWNER=../other-org'
refuses "a malformed repository" "PM_GITEA_REPO is malformed" \
'PM_GITEA_REPO=repo;id'
# --- the assignment itself -------------------------------------------------
refuses "an unknown task mode" "must be 'implement' or 'revise'" \
PM_TASK_MODE=merge
refuses "a non-numeric issue number" "must be a positive integer" \
PM_ISSUE_NUMBER=42x
refuses "issue number zero" "must be a positive integer" \
PM_ISSUE_NUMBER=0
# --- the credentials ------------------------------------------------------
refuses "a missing model credential" "CLAUDE_CODE_OAUTH_TOKEN is not set" \
CLAUDE_CODE_OAUTH_TOKEN=
refuses "a missing git credential" "GITEA_TOKEN is not set" \
GITEA_TOKEN=
echo
if [ "$FAIL" -eq 0 ]; then
echo "probe OK — ${PASS} refusal(s), 0 failure(s)"
exit 0
fi
echo "probe FAILED — ${FAIL} of $((PASS + FAIL)) case(s) did not refuse as expected"
exit 1