An in-cluster consumer reaches a service by its *.svc.cluster.local name, which is by design a different path from how a client outside the cluster reaches the same service. pm-worker's host allowlist accepted TWO values: the cluster-local name and the external hostname gitea.home.keithjackson.dev. The second is removed. It was never needed — nothing sets it — it would route in-cluster traffic out through the Gateway and back for no reason, and as an ACCEPTED value it widened the allowlist to a host reachable over the namespace's world:443 egress rule, which is precisely the injection shape the allowlist exists to refuse. probe-validation.sh now pins that: 20 refusals, including the external hostname, the cluster-local name over https, and the cluster-local name on the wrong port. Also corrects a remark in the handoff entry: I cited cluster-vars' GATEWAY_VIP (192.168.1.53) as suspicious next to the nodes' 192.168.2.x. That was noise — those base cluster-vars values are dev-era leftovers for a decommissioned cluster and say nothing about prod. None of the measured facts depended on it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
131 lines
6.3 KiB
Bash
Executable File
131 lines
6.3 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. infra/agent/build-image.sh runs it inside the
|
|
# built image as well, where the interpreter and the script are the ones that will actually
|
|
# run in the cluster.
|
|
###############################################################################
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[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
|