GL-4: uninstaller gap-closure + operator-key day-0 fold (host-install v1.11.0)
Uninstall: NEW 4b4 removes the self-update artifacts (guarded wrapper, .prev/.new.* A/B slots, rollback unit, limits drop-in — derived from configs/felhom-selfupdate-guarded); enrolled drives under /mnt/felhom-drives unmounted plain-umount-only (busy = warn+guidance, never -l/-f; root bind guarded); both modes end with a KEPT-vs-WIPED statement (drives/PBS/hub record/escrow/vaulted recovery credential live on); guest-only mode prints the vmid's bind-store drives. Key-pin: OPERATOR_KEY_* constants (empty until the operator ceremony) + --operator-pubkey-file (argv-validated; comment=key_id required; file overrides constants) -> authz.signers per the agent SignerKey schema; no-keys-resolved KEEPS preserved signers; verify reports armed/dormant WARN. Harness: +13 static cases + GL4-D parity + GL4-INV + PVE-tier GL4 H-U (full-uninstall dry transcript vs live 9201). 28/28 on felhom-pve; red-proofs RP-1..3 run->fail->revert. GO-LIVE-PACKAGE.md ABSENT AGAIN -> status in CONTEXT.md. Live teardown/armed-pin = GL-6. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -189,6 +189,90 @@ else
|
||||
verdict FAIL "INV-3 usage documents --mode appliance|byo" "rc=$rc"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- GL-4 static tier (key-pin + uninstall parity) ---"
|
||||
|
||||
# GL4-C3: --operator-pubkey-file refusals (each dies at argv-validation, before any host access).
|
||||
GOODKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFakeFakeFakeFakeFakeFakeFakeFakeFakeFakeFake felhom-op-1"
|
||||
printf 'signer %s\n' "$GOODKEY" > "$WORK/keys-badrole"
|
||||
expect_die "GL4-C3a key file: unknown role refused" \
|
||||
"unknown role 'signer' (want operational|recovery)" \
|
||||
-- --customer-id t --mode appliance --operator-pubkey-file "$WORK/keys-badrole"
|
||||
printf 'operational not-a-key-at-all\n' > "$WORK/keys-badline"
|
||||
expect_die "GL4-C3b key file: non-authorized_keys line refused" \
|
||||
"not an authorized_keys line" \
|
||||
-- --customer-id t --mode appliance --operator-pubkey-file "$WORK/keys-badline"
|
||||
printf 'operational ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFakeFakeFake\n' > "$WORK/keys-nocomment"
|
||||
expect_die "GL4-C3c key file: missing key_id comment refused" \
|
||||
"no comment field" \
|
||||
-- --customer-id t --mode appliance --operator-pubkey-file "$WORK/keys-nocomment"
|
||||
printf '# only a comment\n\n' > "$WORK/keys-empty"
|
||||
expect_die "GL4-C3d key file: empty file refused" \
|
||||
"has no key lines" \
|
||||
-- --customer-id t --mode appliance --operator-pubkey-file "$WORK/keys-empty"
|
||||
expect_die "GL4-C3e key file: missing file refused" \
|
||||
"--operator-pubkey-file not found" \
|
||||
-- --customer-id t --mode appliance --operator-pubkey-file "$WORK/keys-nonexistent"
|
||||
|
||||
# GL4-C2 (positive shape, runtime): a VALID key file passes resolution — the script must die LATER
|
||||
# (root/pveum/hub preflight, machine-dependent) and NEVER with a key-file error.
|
||||
printf 'operational %s\nrecovery ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFakeFakeRecovery felhom-rec-1\n' "$GOODKEY" > "$WORK/keys-good"
|
||||
run_script --customer-id t --mode appliance --operator-pubkey-file "$WORK/keys-good" --hub-url https://127.0.0.1:9
|
||||
if [[ $rc -ne 0 && "$out" != *"--operator-pubkey-file"* && "$out" != *"authorized_keys line"* && "$out" != *"unknown role"* ]]; then
|
||||
verdict PASS "GL4-C2 valid key file accepted (dies later, not at key parse)"
|
||||
else
|
||||
verdict FAIL "GL4-C2 valid key file accepted" "rc=$rc; $(echo "$out" | tail -2 | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
# GL4-C4/C5 (grep shapes): the write-guard + override mechanics exist in the script text.
|
||||
if grep -q '^if signers:$' "$SCRIPT" && grep -q "replacing %d preserved authz signer" "$SCRIPT"; then
|
||||
verdict PASS "GL4-C4 signers-only-when-nonempty guard + preserve/replace notice present"
|
||||
else
|
||||
verdict FAIL "GL4-C4 signers-only-when-nonempty guard + preserve/replace notice present"
|
||||
fi
|
||||
if grep -q 'overrides the script.s built-in operator key constants' "$SCRIPT" \
|
||||
&& grep -q 'RESOLVED_OP_ID=""; RESOLVED_OP_LINE=""; RESOLVED_REC_ID=""; RESOLVED_REC_LINE=""' "$SCRIPT"; then
|
||||
verdict PASS "GL4-C5 file-overrides-constants mechanics present (notice + reset)"
|
||||
else
|
||||
verdict FAIL "GL4-C5 file-overrides-constants mechanics present"
|
||||
fi
|
||||
if grep -q 'no operator key pinned' "$SCRIPT" && grep -B1 'no operator key pinned' "$SCRIPT" | grep -q 'log_warn'; then
|
||||
verdict PASS "GL4-C1 verify dormant path is a WARN (not an error)"
|
||||
else
|
||||
verdict FAIL "GL4-C1 verify dormant path is a WARN (not an error)"
|
||||
fi
|
||||
|
||||
# GL4-D: disclosure↔uninstall parity — every host artifact the byo disclosure names must be covered
|
||||
# (removed or explicitly KEPT) in the uninstall section (_guest_drive_note.._end of run_uninstall).
|
||||
ustart=$(grep -n '^_guest_drive_note()' "$SCRIPT" | cut -d: -f1)
|
||||
uend=$(grep -n '^# run_adopt_pool' "$SCRIPT" | cut -d: -f1)
|
||||
if [[ -n "$ustart" && -n "$uend" && "$ustart" -lt "$uend" ]]; then
|
||||
usect=$(sed -n "${ustart},${uend}p" "$SCRIPT")
|
||||
d_missing=""
|
||||
for tok in 'felhom-selfupdate-guarded' 'felhom-agent-rollback.service' 'felhom-agent-limits.conf' \
|
||||
'.prev' 'felhom-mgmt-watchdog' 'felhom-privsep.conf' 'felhom-mkfs-guarded' \
|
||||
'felhom-guest-hook' '/mnt/felhom-drives' 'AGENT_SUDOERS' 'AGENT_STATE_DIR' \
|
||||
'remove_scoped_acl' 'pveum user token remove' 'pveum pool delete' 'STATE_FILE'; do
|
||||
echo "$usect" | grep -qF "$tok" || d_missing+="$tok "
|
||||
done
|
||||
if [[ -z "$d_missing" ]]; then
|
||||
verdict PASS "GL4-D disclosure↔uninstall parity (all artifact tokens covered)"
|
||||
else
|
||||
verdict FAIL "GL4-D disclosure↔uninstall parity" "uncovered: $d_missing"
|
||||
fi
|
||||
else
|
||||
verdict FAIL "GL4-D disclosure↔uninstall parity" "could not locate the uninstall section"
|
||||
fi
|
||||
|
||||
# GL4-INV: no forced/lazy unmount and no format op on the drives root — REAL invocations only
|
||||
# (comment lines and log_* guidance strings legitimately SAY "never umount -l/-f").
|
||||
if ! grep -vE '^[[:space:]]*#|log_(warn|info|dry|error|success|skip)' "$SCRIPT" | grep -E 'umount +-(l|f)' >/dev/null \
|
||||
&& ! grep -vE '^[[:space:]]*#' "$SCRIPT" | grep -E '(mkfs|wipefs) [^|]*/mnt/felhom-drives' >/dev/null; then
|
||||
verdict PASS "GL4-INV no umount -l/-f, no mkfs/wipefs invocation on /mnt/felhom-drives"
|
||||
else
|
||||
verdict FAIL "GL4-INV no umount -l/-f, no mkfs/wipefs invocation on /mnt/felhom-drives"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- PVE tier ---"
|
||||
if ! command -v pveum >/dev/null 2>&1 || [[ "$(id -u)" != 0 ]]; then
|
||||
@@ -201,6 +285,40 @@ else
|
||||
-- --customer-id t --mode byo --cores 4 --memory 8192 \
|
||||
--acl-storages "local definitely-not-a-storage" --dry-run
|
||||
|
||||
# GL4 H-U: FULL uninstall dry transcript (Scenario A). Read-only: every mutation is dry-printed,
|
||||
# the typed confirm takes its dry branch, and the state override keeps the live state.json out.
|
||||
# Requires a Felhom guest to target — resolved from felhom_guests-style detection below.
|
||||
hu_vmid=$(for id in $( { pct list 2>/dev/null; qm list 2>/dev/null; } | awk "{print \$1}" | grep -E "^[0-9]+$" ); do
|
||||
pct config "$id" 2>/dev/null | grep -q "mp=/etc/felhom-bootstrap" && { echo "$id"; break; }
|
||||
done)
|
||||
if [[ -z "$hu_vmid" ]]; then
|
||||
verdict SKIP "GL4 H-U uninstall dry transcript" "no Felhom guest on this host to target"
|
||||
else
|
||||
run_script --uninstall --vmid "$hu_vmid" --dry-run
|
||||
hu_ok=true; hu_why=""
|
||||
[[ $rc -eq 0 ]] || { hu_ok=false; hu_why+="rc=$rc "; }
|
||||
for want in "felhom-selfupdate-guarded" "kept vs wiped" "NEVER wiped"; do
|
||||
[[ "$out" == *"$want"* ]] || { hu_ok=false; hu_why+="missing '$want' "; }
|
||||
done
|
||||
# MUST NOT: forced/lazy unmounts, or ANY destructive op (mkfs/wipefs/rm) on a drive-data
|
||||
# path. Removing the felhom-mkfs-guarded WRAPPER from /usr/local/sbin is legitimate.
|
||||
for bad in "umount -l" "umount -f"; do
|
||||
[[ "$out" != *"$bad"* ]] || { hu_ok=false; hu_why+="contains '$bad' "; }
|
||||
done
|
||||
if echo "$out" | grep -E '(mkfs|wipefs|rm |rm -rf).*/mnt/felhom-drives/' >/dev/null; then
|
||||
hu_ok=false; hu_why+="destructive op on a /mnt/felhom-drives/ path "
|
||||
fi
|
||||
# drive umount lines only when child mounts exist — assert conditionally
|
||||
if findmnt -rn -o TARGET 2>/dev/null | grep -q '^/mnt/felhom-drives/'; then
|
||||
[[ "$out" == *"data stays on the drive"* ]] || { hu_ok=false; hu_why+="missing per-drive umount lines "; }
|
||||
fi
|
||||
if $hu_ok; then
|
||||
verdict PASS "GL4 H-U uninstall dry transcript (guest $hu_vmid; statement + selfupdate removal, no forced ops)"
|
||||
else
|
||||
verdict FAIL "GL4 H-U uninstall dry transcript" "$hu_why"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${FELHOM_TEST_CUSTOMER:-}" && -n "${FELHOM_TEST_PASSFILE:-}" && -r "${FELHOM_TEST_PASSFILE:-}" ]]; then
|
||||
common=(--customer-id "$FELHOM_TEST_CUSTOMER" --passphrase-file "$FELHOM_TEST_PASSFILE" \
|
||||
--vmid 990100 --cores 2 --memory 4096 --dry-run)
|
||||
@@ -218,7 +336,7 @@ else
|
||||
for bad in "4b/8" "chpasswd" "recovery-credential"; do
|
||||
[[ "$out" != *"$bad"* ]] || { hb_ok=false; hb_why+="contains '$bad' "; }
|
||||
done
|
||||
for want in "acknowledge the byo install" "acl storages all present" "-cores 2 -memory 4096"; do
|
||||
for want in "acknowledge the byo install" "acl storages all present" "-cores 2 -memory 4096" "self-update authority"; do
|
||||
[[ "$out" == *"$want"* ]] || { hb_ok=false; hb_why+="missing '$want' "; }
|
||||
done
|
||||
if $hb_ok; then
|
||||
|
||||
Reference in New Issue
Block a user