Hub v0.158.0 code (built, NOT deployed): agent-only vouch, R-895 snapshot alarms, the hub's own 90-day access log (R-1140 A); installer --rotate-recovery-only (R-1141); 02/03/05/09 + break-glass
gates / gates (push) Successful in 6m45s

Hub: the R-120 golden guard judges only a CHANGED golden selection; offsite_snapshots_vanished /
_unexplained (operator-only, hub-origin) on the 07:10 listing; internal/accesslog writes path-only
lines to /data/access-log, 90 days. Installer: --rotate-recovery-only runs step 4b alone (rides
the next installer tag). 03: the orphan check runs after a failed backup (agent 6545ccc). 02: the
allow-list is empty, appdata_folders (controller v0.329.0). All red-proofed.
This commit is contained in:
admin committed 2026-10-17 23:31:34 +02:00
1 parent 5a5486104a
commit 7d64038f02
27 files changed
+1813 -77

No files matched your search

+95 -6
View File
@@ -153,18 +153,22 @@ else
verdict FAIL "C6 byo PVE-major gate present (static grep)"
fi
# INV-1: exactly ONE step_break_glass invocation, and it is gated on MODE == appliance.
# (Comment lines and the function definition itself don't count.)
# INV-1: exactly TWO step_break_glass invocations, each appliance-gated: the install's (gated on
# MODE == appliance) and --rotate-recovery-only's (R-1141, gated on the agent config's
# deployment_mode == appliance). (Comment lines and the function definition itself don't count.)
inv_calls=$(grep -n 'step_break_glass' "$SCRIPT" \
| grep -v -E '^[0-9]+:[[:space:]]*#' \
| grep -v 'step_break_glass()' || true)
inv_count=$(echo "$inv_calls" | grep -c 'step_break_glass' || true)
if [[ "$inv_count" == "1" ]] \
inv_bare=$(echo "$inv_calls" | grep -v 'should_skip break_glass' | cut -d: -f1 || true)
if [[ "$inv_count" == "2" ]] \
&& echo "$inv_calls" | grep -q 'should_skip break_glass' \
&& grep -B4 'should_skip break_glass' "$SCRIPT" | grep -q 'MODE" == "appliance"'; then
verdict PASS "INV-1 break-glass call site: exactly one, appliance-gated"
&& grep -B4 'should_skip break_glass' "$SCRIPT" | grep -q 'MODE" == "appliance"' \
&& [[ -n "$inv_bare" ]] \
&& sed -n "$((inv_bare-1))p" "$SCRIPT" | grep -q 'ROTATE_CFG_MODE" == "appliance"'; then
verdict PASS "INV-1 break-glass call sites: exactly two, both appliance-gated"
else
verdict FAIL "INV-1 break-glass call site: exactly one, appliance-gated" "count=$inv_count; calls: $(echo "$inv_calls" | tr '\n' ' ')"
verdict FAIL "INV-1 break-glass call sites: exactly two, both appliance-gated" "count=$inv_count; calls: $(echo "$inv_calls" | tr '\n' ' ')"
fi
# INV-2: chpasswd is unreachable outside step_break_glass's body (non-comment occurrences only).
@@ -338,6 +342,91 @@ else
verdict FAIL "GL8-F1 uninstall purges the agent config dir (every copy, R-275)"
fi
echo ""
echo "--- R-1141 --rotate-recovery-only (step 4b alone) ---"
# Runs anywhere: refusals die before any host access; the dry-run reads a FAKE agent config through the
# test-only FELHOM_AGENT_CONFIG override and points the hub at a closed port. A chpasswd/systemctl/pct
# PATH shim records any call — a dry run must make none of them.
RK="FAKE-HOST-KEY-r1141-never-printed"
mkdir -p "$WORK/r1141shim"
for c in chpasswd pct qm pveum systemctl apt-get install; do
printf '#!/bin/sh\necho "%s $*" >> "%s/r1141shim/calls"\nexit 1\n' "$c" "$WORK" > "$WORK/r1141shim/$c"
chmod 0755 "$WORK/r1141shim/$c"
done
r1141_cfg() { # <file> <deployment_mode|-> <api_key>
python3 - "$1" "$2" "$3" <<'PY'
import json, sys
f, mode, key = sys.argv[1:]
d = {"hub": {"url": "http://127.0.0.1:9", "host_id": "h-test", "api_key": key}}
if mode != "-":
d["deployment_mode"] = mode
json.dump(d, open(f, "w"))
PY
chmod 0600 "$1"
}
r1141_run() { # <config> <args...>
local cfg="$1"; shift
set +e
out=$(FELHOM_INSTALL_STATE_DIR="$STATE_OVERRIDE_ENV" FELHOM_AGENT_CONFIG="$cfg" PATH="$WORK/r1141shim:$PATH" \
bash "$SCRIPT" --rotate-recovery-only "$@" </dev/null 2>&1)
rc=$?
set -e
}
r1141_expect_die() { # <name> <msg> <config> <args...>
local name="$1" msg="$2" cfg="$3"; shift 3
r1141_run "$cfg" "$@"
if [[ $rc -ne 0 && "$out" == *"$msg"* && "$out" != *"4b/8"* ]]; then
verdict PASS "$name"
else
verdict FAIL "$name" "rc=$rc; wanted '$msg', no 4b/8; got: $(echo "$out" | tail -3 | tr '\n' ' ')"
fi
}
r1141_cfg "$WORK/r1141-app.json" appliance "$RK"
r1141_cfg "$WORK/r1141-byo.json" byo "$RK"
r1141_cfg "$WORK/r1141-nomode.json" - "$RK"
r1141_cfg "$WORK/r1141-nokey.json" appliance ""
printf 'not json' > "$WORK/r1141-bad.json"
r1141_expect_die "R1141-a --mode byo refused" "is not allowed in byo mode" "$WORK/r1141-app.json" --mode byo --dry-run
r1141_expect_die "R1141-b with --uninstall refused" "--rotate-recovery-only runs alone" "$WORK/r1141-app.json" --uninstall --dry-run
r1141_expect_die "R1141-c with --resume refused" "--rotate-recovery-only runs alone" "$WORK/r1141-app.json" --resume --dry-run
r1141_expect_die "R1141-d config deployment_mode=byo refused" "deployment_mode=byo, not appliance" "$WORK/r1141-byo.json" --dry-run
r1141_expect_die "R1141-e config without deployment_mode refused (fail-safe)" "deployment_mode=<absent>, not appliance" "$WORK/r1141-nomode.json" --dry-run
r1141_expect_die "R1141-f config without a host key refused" "no hub.host_id/hub.api_key" "$WORK/r1141-nokey.json" --dry-run
r1141_expect_die "R1141-g config not JSON refused" "agent config is not valid JSON" "$WORK/r1141-bad.json" --dry-run
r1141_expect_die "R1141-h config missing refused" "agent config not readable" "$WORK/r1141-absent.json" --dry-run
if [[ "$(id -u)" != 0 ]]; then
r1141_expect_die "R1141-i a real run needs root" "must run as root" "$WORK/r1141-app.json"
else
verdict SKIP "R1141-i a real run needs root" "running as root — the refusal cannot be shown here"
fi
# The positive dry run: step 4b's dry line only, the host key never printed, no other step, no host
# command, no state written.
rm -f "$WORK/r1141shim/calls" "$STATE_OVERRIDE/state.json"
r1141_run "$WORK/r1141-app.json" --dry-run
r_ok=true; r_why=""
[[ $rc -eq 0 ]] || { r_ok=false; r_why+="rc=$rc "; }
for want in "mode=rotate-recovery-only" "4b/8 break-glass credential" \
"PUT http://127.0.0.1:9/api/v1/hosts/h-test/recovery-credential" "a real run would stop here"; do
[[ "$out" == *"$want"* ]] || { r_ok=false; r_why+="missing '$want' "; }
done
for bad in "$RK" "1/8" "2/8" "3/8" "4/8" "5/8" "6/8" "7/8" "8/8" "agent install" "PASSPHRASE" "retrieval passphrase"; do
[[ "$out" != *"$bad"* ]] || { r_ok=false; r_why+="contains '$bad' "; }
done
[[ ! -s "$WORK/r1141shim/calls" ]] || { r_ok=false; r_why+="host commands called: $(tr '\n' ';' < "$WORK/r1141shim/calls") "; }
[[ ! -e "$STATE_OVERRIDE/state.json" ]] || { r_ok=false; r_why+="state.json written "; }
if $r_ok; then
verdict PASS "R1141-j dry run: step 4b only, host key not printed, no host command, no state"
else
verdict FAIL "R1141-j dry run: step 4b only, host key not printed, no host command, no state" "$r_why"
fi
# Usage documents the flag (the real -h path).
run_script -h
if [[ $rc -eq 0 && "$out" == *"--rotate-recovery-only"* ]]; then
verdict PASS "R1141-k usage documents --rotate-recovery-only"
else
verdict FAIL "R1141-k usage documents --rotate-recovery-only" "rc=$rc"
fi
echo ""
echo "--- F5 appliance guest auto-sizing (VALIDATION-n100) ---"
# The auto-size log line is emitted right after mode validation (BEFORE any PVE/hub contact), so it