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
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:
@@ -1,3 +1,16 @@
|
||||
## 2026-10-18 — `felhom-host-install.sh --rotate-recovery-only` (R-1141); not yet tagged
|
||||
|
||||
A new flag runs step 4b ALONE: a new random root@pam password, set with chpasswd and re-vaulted to the hub with the
|
||||
host key from the agent's own config. It never installs or changes the agent, its config, the root files or a guest;
|
||||
no passphrase, no customer id. Refuses byo (`--mode byo`, and an agent config whose `deployment_mode` is not
|
||||
`appliance` — absent counts as byo), refuses with --uninstall/--adopt-pool/--rescope-acl/--preflight-only/--resume,
|
||||
needs root (except --dry-run), and asks the hub's `/healthz` before the password changes. `step_break_glass` moved,
|
||||
byte-identical, ahead of the argument parse (the new mode is dispatched before the install's mode validation).
|
||||
Harness: INV-1 now counts two appliance-gated call sites; new cases R1141-a..k; red-proved by four mutations. The
|
||||
default install path: same harness verdicts old vs new, identical non-PVE transcripts for 9 argv sets, and on demo-hp
|
||||
an identical `--preflight-only` transcript (except a live free-RAM number and a line number). Pushing to `main`
|
||||
publishes nothing — this rides the next `installer-v…` tag. SCRIPT_VERSION unchanged.
|
||||
|
||||
## 2026-10-17 — the uninstall removes agent v0.159.0's new root helper (R-1012); not yet tagged
|
||||
|
||||
`felhom-host-install.sh --uninstall` removes `/usr/local/sbin/felhom-thin-check`, the read-only orphan-thin check the
|
||||
|
||||
+143
-52
@@ -173,6 +173,16 @@
|
||||
# --acl-storages "a b c" override the storages the scoped ACL grants Datastore.* on
|
||||
# (default: "local local-lvm felhom-pbs"). Used by fresh install + --rescope-acl.
|
||||
#
|
||||
# Rotate the console password ALONE (R-1141 — no passphrase, no customer id):
|
||||
# --rotate-recovery-only step 4b and nothing else: a new random root@pam password, set with chpasswd and
|
||||
# re-vaulted to the hub with the host key in the agent's own config. Never installs
|
||||
# or changes the agent, its config, the root files, a guest or the install state
|
||||
# (except the break_glass mark). Refuses byo (also a config with deployment_mode
|
||||
# other than "appliance"), refuses with any other mode flag, and checks the hub
|
||||
# answers before it changes the password. Takes --dry-run. The hub is the one in
|
||||
# the agent config (--hub-url is not used). Do NOT use it on a host whose
|
||||
# root@pam was set by hand to a known password — it replaces it with a random one.
|
||||
#
|
||||
# State (idempotent/resumable): /var/lib/felhom-install/state.json
|
||||
# Agent config written 0600 to the systemd unit's -config path
|
||||
# (auto-detected; else /etc/felhom-agent/agent.json).
|
||||
@@ -270,6 +280,7 @@ REMOVE_GOLDEN=false # --remove-golden: also delete the golden vzdump during
|
||||
ADOPT_POOL=false # --adopt-pool: retrofit an EXISTING Felhom guest into the felhom pool (non-destructive)
|
||||
RESCOPE_ACL=false # --rescope-acl: migrate an existing install from the broad-/ token to the scoped ACL
|
||||
ROTATE_RECOVERY=false # --rotate-recovery: regenerate + re-vault the break-glass root@pam password (TASK G1)
|
||||
ROTATE_RECOVERY_ONLY=false # --rotate-recovery-only: step 4b ALONE, from the agent config's hub key (R-1141)
|
||||
ENABLE_OOB=false # the OOB belt (TASK H1). DEFAULT-ON for appliance mode since v1.25.0 (F9 ruling
|
||||
# 2026-07-23: an operator-owned box must be reachable OOB) — see the appliance
|
||||
# default below; --enable-oob forces it, --no-oob opts an appliance box out.
|
||||
@@ -1605,6 +1616,60 @@ run_rescope_acl() {
|
||||
return 0
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# STEP 4b — break-glass credential (TASK G1): generate + set + vault the root@pam console password
|
||||
#-------------------------------------------------------------------------------
|
||||
# The human fallback for when BOTH the sshd path AND the agent-independent auto-heal (layers 1+2) have
|
||||
# failed: a strong root@pam password lets the operator reach the PVE WEB CONSOLE (pveproxy :8006 — a
|
||||
# failure domain distinct from sshd) and run the one-line /run/sshd fix. Generated with strong entropy,
|
||||
# set via chpasswd, and vaulted to the hub over the enroll-authenticated channel (host api_key). The
|
||||
# password is NEVER logged, printed, or written to any file — it goes stdin→chpasswd and stdin→curl
|
||||
# only (SPIKE-felhom-sshd finding #9 / TASK G1 trap 3). Idempotent: skipped if already vaulted unless
|
||||
# --rotate-recovery (a re-set would strand the operator's saved copy).
|
||||
# Defined HERE, ahead of the argument parse (R-1141): --rotate-recovery-only is dispatched before the
|
||||
# install's mode validation and calls it. Moved unchanged; the install still calls it from Main.
|
||||
step_break_glass() {
|
||||
log_step "4b/8 break-glass credential (root@pam console password → hub vault)"
|
||||
if [[ -z "${HOST_ID:-}" || -z "${HOST_API_KEY:-}" ]]; then
|
||||
log_warn " no host_id/api_key (enroll skipped?) — cannot vault a recovery credential; skipping"
|
||||
return 0
|
||||
fi
|
||||
if $DRY_RUN; then
|
||||
log_dry "openssl rand → strong root@pam password (never logged) ; chpasswd ; PUT $HUB_URL/api/v1/hosts/$HOST_ID/recovery-credential (Bearer host key)"
|
||||
_state_mark break_glass; return 0
|
||||
fi
|
||||
if _state_has break_glass && ! $ROTATE_RECOVERY; then
|
||||
log_skip " recovery credential already vaulted (use --rotate-recovery to regenerate)"
|
||||
return 0
|
||||
fi
|
||||
# Strong password: 24 url-safe bytes (~144 bits). Kept ONLY in a local shell var, never on disk.
|
||||
local newpw
|
||||
newpw=$(openssl rand -base64 24 2>/dev/null | tr -d '\n' | tr '+/' '-_')
|
||||
[[ ${#newpw} -ge 24 ]] || die "failed to generate a strong recovery password"
|
||||
# Set root@pam (= the Linux root user on PVE) via chpasswd on STDIN — no argv, no log.
|
||||
if ! printf 'root:%s\n' "$newpw" | chpasswd 2>/dev/null; then
|
||||
newpw="" # scrub
|
||||
die "chpasswd failed to set the root@pam recovery password"
|
||||
fi
|
||||
# Vault to the hub over the host-key-authenticated channel; password only on stdin (-d @-).
|
||||
local code
|
||||
code=$(printf '{"username":"root@pam","password":"%s"}' "$newpw" \
|
||||
| curl -sS -o /dev/null -w '%{http_code}' -X PUT \
|
||||
"$HUB_URL/api/v1/hosts/$HOST_ID/recovery-credential" \
|
||||
-H "Authorization: Bearer $HOST_API_KEY" -H 'Content-Type: application/json' -d @- 2>/dev/null)
|
||||
newpw="" # scrub the plaintext from the shell var the moment it is vaulted
|
||||
case "$code" in
|
||||
200) log_success " root@pam password set + vaulted to the hub (retrieve via the operator /admin path; never logged here)"
|
||||
# Drill F-8: say this OUT LOUD — the operator was live-surprised by a 401 at the PVE
|
||||
# GUI. Pointer only, never the credential.
|
||||
log_warn " NOTE: the root@pam password just CHANGED — the old one now fails at the PVE web GUI (:8006)."
|
||||
log_warn " Retrieve the new one at hub → host page (vaulted recovery credential)." ;;
|
||||
401|403) die "recovery-credential vault rejected ($code) — host key/authorization problem" ;;
|
||||
*) die "recovery-credential vault failed (HTTP $code)" ;;
|
||||
esac
|
||||
_state_mark break_glass
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Arg parse
|
||||
#-------------------------------------------------------------------------------
|
||||
@@ -1638,6 +1703,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--adopt-pool) ADOPT_POOL=true; shift ;;
|
||||
--rescope-acl) RESCOPE_ACL=true; shift ;;
|
||||
--rotate-recovery) ROTATE_RECOVERY=true; shift ;;
|
||||
--rotate-recovery-only) ROTATE_RECOVERY_ONLY=true; shift ;;
|
||||
--enable-oob) ENABLE_OOB=true; shift ;;
|
||||
--no-oob) NO_OOB=true; shift ;;
|
||||
--acl-storages) read -ra PVE_STORAGES <<< "$2"; shift 2 ;;
|
||||
@@ -1649,6 +1715,83 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
#===============================================================================
|
||||
# ROTATE-RECOVERY-ONLY MODE (R-1141) — step 4b and NOTHING else. The full re-run with --rotate-recovery
|
||||
# also reinstalls the vouched agent and the root files (on a box ahead of the vouched agent it would move
|
||||
# the agent back). This mode reads the hub URL, host id and host key from the agent's own config and
|
||||
# calls step_break_glass; no passphrase, no hub enroll, no agent/config/guest step. Dispatched FIRST, so
|
||||
# a combination with another mode flag is refused before that mode can run.
|
||||
#===============================================================================
|
||||
if $ROTATE_RECOVERY_ONLY; then
|
||||
if $UNINSTALL || $ADOPT_POOL || $RESCOPE_ACL || $PREFLIGHT_ONLY || $RESUME; then
|
||||
die "--rotate-recovery-only runs alone (not with --uninstall, --adopt-pool, --rescope-acl, --preflight-only or --resume) — nothing changed"
|
||||
fi
|
||||
if [[ -n "$MODE" && "$MODE" != "appliance" ]]; then
|
||||
die "--rotate-recovery-only is not allowed in $MODE mode (byo never touches root@pam — there is no recovery credential to rotate)"
|
||||
fi
|
||||
$DRY_RUN || [[ $EUID -eq 0 ]] || die "must run as root"
|
||||
echo ""
|
||||
log_info "felhom-host-install v${SCRIPT_VERSION} — mode=rotate-recovery-only"
|
||||
$DRY_RUN && log_warn "DRY-RUN: no mutations will be performed"
|
||||
echo ""
|
||||
# The agent config (0600, root): the same path resolution as the preflight. FELHOM_AGENT_CONFIG is a
|
||||
# test-harness-only override (hostinstall-mode-harness.sh), like FELHOM_INSTALL_STATE_DIR.
|
||||
AGENT_CONFIG="${FELHOM_AGENT_CONFIG:-}"
|
||||
if [[ -z "$AGENT_CONFIG" ]] && systemctl cat felhom-agent >/dev/null 2>&1; then
|
||||
AGENT_CONFIG=$(systemctl cat felhom-agent 2>/dev/null | grep -oP '(?<=-config )\S+' | head -1)
|
||||
fi
|
||||
[[ -n "$AGENT_CONFIG" ]] || AGENT_CONFIG="/etc/felhom-agent/agent.json"
|
||||
[[ -r "$AGENT_CONFIG" ]] || die "agent config not readable: $AGENT_CONFIG — nothing changed (is this a Felhom host? run as root)"
|
||||
log_info " agent config: $AGENT_CONFIG"
|
||||
# _rotate_cfg KEY — one string from the config (deployment_mode, or hub.<KEY>); the path rides the
|
||||
# environment, the host key goes stdout→shell var only (never argv, never printed). Exit 3 = not JSON.
|
||||
_rotate_cfg() {
|
||||
AGENT_CONFIG="$AGENT_CONFIG" python3 -c '
|
||||
import json, os, sys
|
||||
try:
|
||||
d = json.load(open(os.environ["AGENT_CONFIG"]))
|
||||
except Exception:
|
||||
sys.exit(3)
|
||||
k = sys.argv[1]
|
||||
v = d.get("deployment_mode", "") if k == "deployment_mode" else (d.get("hub") or {}).get(k, "")
|
||||
print(v if isinstance(v, str) else "")' "$1" 2>/dev/null
|
||||
}
|
||||
ROTATE_CFG_MODE=$(_rotate_cfg deployment_mode) || die "agent config is not valid JSON: $AGENT_CONFIG — nothing changed"
|
||||
HOST_ID=$(_rotate_cfg host_id) || die "agent config is not valid JSON: $AGENT_CONFIG — nothing changed"
|
||||
HOST_API_KEY=$(_rotate_cfg api_key) || die "agent config is not valid JSON: $AGENT_CONFIG — nothing changed"
|
||||
_rotate_hub=$(_rotate_cfg url) || die "agent config is not valid JSON: $AGENT_CONFIG — nothing changed"
|
||||
[[ -n "$_rotate_hub" ]] && HUB_URL="${_rotate_hub%/}"
|
||||
# Fail-safe like the agent: anything but an explicit "appliance" is treated as byo (absent/typo → byo).
|
||||
if [[ "$ROTATE_CFG_MODE" != "appliance" ]]; then
|
||||
die "this host's agent config says deployment_mode=${ROTATE_CFG_MODE:-<absent>}, not appliance — root@pam belongs to the host owner; nothing changed"
|
||||
fi
|
||||
[[ -n "$HOST_ID" && -n "$HOST_API_KEY" ]] || die "agent config has no hub.host_id/hub.api_key — cannot vault a recovery credential; nothing changed"
|
||||
log_info " host_id: $HOST_ID (host key read from the agent config, not logged); hub: $HUB_URL"
|
||||
# Ask the hub first: a vault that fails AFTER chpasswd leaves a password nobody holds.
|
||||
_rotate_hc=$(http_code --max-time 15 "$HUB_URL/healthz" || true); _rotate_hc="${_rotate_hc: -3}"
|
||||
if [[ "$_rotate_hc" != "200" ]]; then
|
||||
if $DRY_RUN; then
|
||||
log_warn " hub $HUB_URL/healthz answered HTTP ${_rotate_hc:-000} — a real run would stop here, before the password changes"
|
||||
else
|
||||
die "hub $HUB_URL/healthz answered HTTP ${_rotate_hc:-000} — the password was NOT changed (a vault after it would fail)"
|
||||
fi
|
||||
else
|
||||
log_info " hub answers (healthz 200)"
|
||||
fi
|
||||
ROTATE_RECOVERY=true # the step's own "already vaulted" skip must not apply: rotating is the point
|
||||
if [[ "$ROTATE_CFG_MODE" == "appliance" ]]; then
|
||||
step_break_glass
|
||||
fi
|
||||
HOST_API_KEY=""
|
||||
echo ""
|
||||
if $DRY_RUN; then
|
||||
log_warn "DRY-RUN: nothing above was actually executed."
|
||||
else
|
||||
log_success "ROTATE-RECOVERY-ONLY complete — only root@pam's password changed (and its hub vault); agent, config, root files and guests untouched."
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#===============================================================================
|
||||
# UNINSTALL MODE — local host teardown (no hub contact, no passphrase). Dispatched early,
|
||||
# before any provision/DR logic, and does not require --customer-id.
|
||||
@@ -2382,58 +2525,6 @@ step_enroll() {
|
||||
_state_mark enroll
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# STEP 4b — break-glass credential (TASK G1): generate + set + vault the root@pam console password
|
||||
#-------------------------------------------------------------------------------
|
||||
# The human fallback for when BOTH the sshd path AND the agent-independent auto-heal (layers 1+2) have
|
||||
# failed: a strong root@pam password lets the operator reach the PVE WEB CONSOLE (pveproxy :8006 — a
|
||||
# failure domain distinct from sshd) and run the one-line /run/sshd fix. Generated with strong entropy,
|
||||
# set via chpasswd, and vaulted to the hub over the enroll-authenticated channel (host api_key). The
|
||||
# password is NEVER logged, printed, or written to any file — it goes stdin→chpasswd and stdin→curl
|
||||
# only (SPIKE-felhom-sshd finding #9 / TASK G1 trap 3). Idempotent: skipped if already vaulted unless
|
||||
# --rotate-recovery (a re-set would strand the operator's saved copy).
|
||||
step_break_glass() {
|
||||
log_step "4b/8 break-glass credential (root@pam console password → hub vault)"
|
||||
if [[ -z "${HOST_ID:-}" || -z "${HOST_API_KEY:-}" ]]; then
|
||||
log_warn " no host_id/api_key (enroll skipped?) — cannot vault a recovery credential; skipping"
|
||||
return 0
|
||||
fi
|
||||
if $DRY_RUN; then
|
||||
log_dry "openssl rand → strong root@pam password (never logged) ; chpasswd ; PUT $HUB_URL/api/v1/hosts/$HOST_ID/recovery-credential (Bearer host key)"
|
||||
_state_mark break_glass; return 0
|
||||
fi
|
||||
if _state_has break_glass && ! $ROTATE_RECOVERY; then
|
||||
log_skip " recovery credential already vaulted (use --rotate-recovery to regenerate)"
|
||||
return 0
|
||||
fi
|
||||
# Strong password: 24 url-safe bytes (~144 bits). Kept ONLY in a local shell var, never on disk.
|
||||
local newpw
|
||||
newpw=$(openssl rand -base64 24 2>/dev/null | tr -d '\n' | tr '+/' '-_')
|
||||
[[ ${#newpw} -ge 24 ]] || die "failed to generate a strong recovery password"
|
||||
# Set root@pam (= the Linux root user on PVE) via chpasswd on STDIN — no argv, no log.
|
||||
if ! printf 'root:%s\n' "$newpw" | chpasswd 2>/dev/null; then
|
||||
newpw="" # scrub
|
||||
die "chpasswd failed to set the root@pam recovery password"
|
||||
fi
|
||||
# Vault to the hub over the host-key-authenticated channel; password only on stdin (-d @-).
|
||||
local code
|
||||
code=$(printf '{"username":"root@pam","password":"%s"}' "$newpw" \
|
||||
| curl -sS -o /dev/null -w '%{http_code}' -X PUT \
|
||||
"$HUB_URL/api/v1/hosts/$HOST_ID/recovery-credential" \
|
||||
-H "Authorization: Bearer $HOST_API_KEY" -H 'Content-Type: application/json' -d @- 2>/dev/null)
|
||||
newpw="" # scrub the plaintext from the shell var the moment it is vaulted
|
||||
case "$code" in
|
||||
200) log_success " root@pam password set + vaulted to the hub (retrieve via the operator /admin path; never logged here)"
|
||||
# Drill F-8: say this OUT LOUD — the operator was live-surprised by a 401 at the PVE
|
||||
# GUI. Pointer only, never the credential.
|
||||
log_warn " NOTE: the root@pam password just CHANGED — the old one now fails at the PVE web GUI (:8006)."
|
||||
log_warn " Retrieve the new one at hub → host page (vaulted recovery credential)." ;;
|
||||
401|403) die "recovery-credential vault rejected ($code) — host key/authorization problem" ;;
|
||||
*) die "recovery-credential vault failed (HTTP $code)" ;;
|
||||
esac
|
||||
_state_mark break_glass
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# STEP 5 — agent install: fetch+verify the binary, ensure the service user, sudoers, unit
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user