Installer step 4b: vault first, then set, then a PVE login (R-1145); host key off curl's argv (review F4); harness green and a gate (R-1146); hub 0.158.0 deploy evidence; independent review + verdicts
gates / gates (push) Successful in 7m23s
gates / gates (push) Successful in 7m23s
Live on demo-hp (--rotate-recovery-only): vault time moved, new value 200, old 401. Harness 52/0; the R1145 cases fail against HEAD. Publishes only with the next installer tag.
This commit is contained in:
@@ -325,12 +325,27 @@ fi
|
||||
|
||||
# GL8-F6: the byo :53 gate REFUSES (dies) with the remediation and mutates nothing — no
|
||||
# stop/mask/kill of a :53 service anywhere in the script.
|
||||
# R-1146 (2026-10-18): the check is judged by WHERE a line is, not by a fixed window. The gate's R-300 hint grew the
|
||||
# byo `if` beyond 6 lines above the refusal, and R-300 made the UNINSTALL turn off Felhom's OWN dnsmasq on purpose.
|
||||
# Allowed: a disable inside run_uninstall (ours, R-300) or inside the gate's _hint="…" text (an instruction to the
|
||||
# owner, never executed). Any other stop/mask/disable/kill of a resolver, anywhere, still FAILS.
|
||||
gl8_f6_offenders=$(awk '
|
||||
/^[a-zA-Z_]+\(\) *\{/ { fn = $1 }
|
||||
/_hint="/ { inhint = 1 }
|
||||
!/^[[:space:]]*#/ && /(systemctl (stop|mask|disable)|kill|pkill).*(dnsmasq|:53|resolv)/ {
|
||||
if (fn != "run_uninstall()" && !inhint) print NR": "$0
|
||||
}
|
||||
inhint && /"[[:space:]]*$/ && !/_hint="[[:space:]]*$/ { inhint = 0 }
|
||||
' "$SCRIPT")
|
||||
gl8_f6_byo_before=$(awk '
|
||||
/if \[\[ "\$MODE" == "byo" \]\]; then/ { last = NR }
|
||||
/die "a resolver is already bound to :53 on this host — Felhom needs/ { print (last && NR - last <= 40) ? "yes" : "no"; exit }
|
||||
' "$SCRIPT")
|
||||
if grep -q 'a resolver is already bound to :53 on this host — Felhom needs the guest reachable' "$SCRIPT" \
|
||||
&& grep -B6 'a resolver is already bound to :53 on this host — Felhom needs' "$SCRIPT" | grep -q 'MODE" == "byo"' \
|
||||
&& ! grep -vE '^[[:space:]]*#' "$SCRIPT" | grep -E '(systemctl (stop|mask|disable)|kill|pkill).*(dnsmasq|:53|resolv)' >/dev/null; then
|
||||
&& [[ "$gl8_f6_byo_before" == "yes" ]] && [[ -z "$gl8_f6_offenders" ]]; then
|
||||
verdict PASS "GL8-F6 byo :53 gate refuses+instructs, never mutates the owner's resolver"
|
||||
else
|
||||
verdict FAIL "GL8-F6 byo :53 gate refuses+instructs, never mutates the owner's resolver"
|
||||
verdict FAIL "GL8-F6 byo :53 gate refuses+instructs, never mutates the owner's resolver" "byo-gate=${gl8_f6_byo_before:-none} offenders=${gl8_f6_offenders:-none}"
|
||||
fi
|
||||
|
||||
# GL8-F1 / R-275: the agent config and EVERY copy of it go — the agent's own config dir is purged as a
|
||||
@@ -427,6 +442,71 @@ else
|
||||
verdict FAIL "R1141-k usage documents --rotate-recovery-only" "rc=$rc"
|
||||
fi
|
||||
|
||||
# ── R-1145 (2026-10-18): step 4b is VAULT FIRST, THEN SET. The REAL function (cut out of the script) runs with fake
|
||||
# openssl/curl/chpasswd/sleep on PATH and a fake shadow file; each fake appends one word to a calls file, so the
|
||||
# ORDER is what is judged. The fake password must never appear in the step's output or any fake's argv.
|
||||
r1145_dir="$WORK/r1145"; mkdir -p "$r1145_dir/bin"
|
||||
R1145_PW="FAKEPW-r1145-AAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
cat > "$r1145_dir/bin/openssl" <<EOF
|
||||
#!/bin/bash
|
||||
echo "$R1145_PW"
|
||||
EOF
|
||||
cat > "$r1145_dir/bin/curl" <<'EOF'
|
||||
#!/bin/bash
|
||||
cat >/dev/null # the body (password) arrives on stdin and goes nowhere
|
||||
echo "argv: $*" >> "$R1145_DIR/argv"
|
||||
case "$*" in
|
||||
*recovery-credential*) echo vault >> "$R1145_DIR/calls"; printf '%s' "$FAKE_VAULT" ;;
|
||||
*access/ticket*) echo login >> "$R1145_DIR/calls"; printf '%s' "$FAKE_LOGIN" ;;
|
||||
esac
|
||||
EOF
|
||||
cat > "$r1145_dir/bin/chpasswd" <<'EOF'
|
||||
#!/bin/bash
|
||||
cat >/dev/null
|
||||
echo chpasswd >> "$R1145_DIR/calls"
|
||||
[[ "$FAKE_CHPASSWD" == ok ]] || exit 1
|
||||
echo "root:\$y\$changed:20000::::::" > "$R1145_SHADOW"
|
||||
EOF
|
||||
printf '#!/bin/bash\nexit 0\n' > "$r1145_dir/bin/sleep"
|
||||
chmod +x "$r1145_dir/bin/"*
|
||||
sed -n '/^_root_shadow_fingerprint() {/,/^}/p; /^step_break_glass() {/,/^}/p' "$SCRIPT" > "$r1145_dir/step.sh"
|
||||
# r1145_run <vault code> <chpasswd ok|fail> <login code> — sets r_rc, r_out, r_calls, r_shadow_changed
|
||||
r1145_run() {
|
||||
: > "$r1145_dir/calls"; : > "$r1145_dir/argv"; echo 'root:$y$old:20000::::::' > "$r1145_dir/shadow"
|
||||
set +e
|
||||
r_out=$(PATH="$r1145_dir/bin:$PATH" R1145_DIR="$r1145_dir" R1145_SHADOW="$r1145_dir/shadow" \
|
||||
FELHOM_SHADOW_FILE="$r1145_dir/shadow" FAKE_VAULT="$1" FAKE_CHPASSWD="$2" FAKE_LOGIN="$3" bash -c '
|
||||
log_step() { echo "STEP $*"; }; log_info() { echo "INFO $*"; }; log_warn() { echo "WARN $*"; }
|
||||
log_error() { echo "ERROR $*"; }; log_success() { echo "OK $*"; }; log_skip() { echo "SKIP $*"; }; log_dry() { echo "DRY $*"; }
|
||||
die() { echo "DIE $*"; exit 1; }; _state_mark() { echo "MARK $*"; }; _state_has() { return 1; }
|
||||
DRY_RUN=false; ROTATE_RECOVERY=true; HOST_ID=h-test; HOST_API_KEY=k-test; HUB_URL=http://127.0.0.1:9
|
||||
source "'"$r1145_dir/step.sh"'"; step_break_glass' 2>&1)
|
||||
r_rc=$?
|
||||
set -e
|
||||
r_calls=$(tr '\n' ' ' < "$r1145_dir/calls")
|
||||
grep -q 'old' "$r1145_dir/shadow" && r_shadow_changed=no || r_shadow_changed=yes
|
||||
}
|
||||
r1145_check() { # <name> <want rc 0|1> <want calls> <want shadow changed> <want output substring>
|
||||
local ok=true why=""
|
||||
[[ "$r_rc" == "$2" || ( "$2" == 1 && "$r_rc" != 0 ) ]] || { ok=false; why+="rc=$r_rc "; }
|
||||
[[ "$r_calls" == "$3" ]] || { ok=false; why+="calls='$r_calls' want '$3' "; }
|
||||
[[ "$r_shadow_changed" == "$4" ]] || { ok=false; why+="shadow changed=$r_shadow_changed "; }
|
||||
[[ "$r_out" == *"$5"* ]] || { ok=false; why+="missing '$5' "; }
|
||||
if [[ "$r_out" == *"$R1145_PW"* ]] || grep -qF "$R1145_PW" "$r1145_dir/argv"; then ok=false; why+="the password leaked "; fi
|
||||
if grep -qF "k-test" "$r1145_dir/argv" || [[ "$r_out" == *"k-test"* ]]; then ok=false; why+="the host key is in curl's argv or the output "; fi
|
||||
if $ok; then verdict PASS "$1"; else verdict FAIL "$1" "$why"; fi
|
||||
}
|
||||
r1145_run 500 ok 200
|
||||
r1145_check "R1145-a vault fails → the password is NOT touched" 1 "vault " no "the root@pam password was NOT changed"
|
||||
r1145_run 401 ok 200
|
||||
r1145_check "R1145-e a stale host key (vault 401) → the password is NOT touched (review F1)" 1 "vault " no "vault rejected (401)"
|
||||
r1145_run 200 ok 200
|
||||
r1145_check "R1145-b vault, then set, then the PVE login — in that order" 0 "vault chpasswd login " yes "the PVE login takes it"
|
||||
r1145_run 200 fail 200
|
||||
r1145_check "R1145-c set fails twice after the vault → the state is said, root unchanged" 1 "vault chpasswd chpasswd " no "STATE: the hub now holds a recovery password that root@pam does NOT have"
|
||||
r1145_run 200 ok 401
|
||||
r1145_check "R1145-d the PVE login refuses the new value → die, the state is said" 1 "vault chpasswd login " yes "the PVE web login answered HTTP 401"
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user