host-install: --preserve-state-from + populated-host leaf guard (prevention B.2)
--preserve-state-from DIR carries the prior local-api.{crt,key}+local-tokens.log into the agent
state dir (validates the leaf parses) so the pinned fp stays stable across a reinstall — no
re-bootstrap. Populated-host guard: refuse to proceed leaf-less when the host already has guests
unless --preserve-state-from or --allow-new-leaf is given (converts the 2026-06-28 silent footgun
into a hard stop). bash -n clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pg8ANF97SEeKYSN5Jxw3qJ
This commit is contained in:
@@ -47,6 +47,13 @@
|
||||
# (default: secure no-echo prompt)
|
||||
# --preserve-from PATH merge non-Day-0 sections (privileged/storage/backup/
|
||||
# local_api/authz/lan_resolver) from an existing config
|
||||
# --preserve-state-from PATH carry the prior agent leaf+key+token-store (local-api.crt/key,
|
||||
# local-tokens.log) over so the pinned fingerprint STAYS STABLE across a
|
||||
# reinstall (no controller re-bootstrap). Use an aside copy of the old
|
||||
# /var/lib/felhom-agent.
|
||||
# --allow-new-leaf opt in to REGENERATE the agent leaf on a host that already has guests
|
||||
# (the populated-host guard otherwise refuses; every guest must then be
|
||||
# re-bootstrapped — only use intentionally).
|
||||
# --force allow provisioning over an EXISTING vmid (destructive)
|
||||
# --skip-provision install + configure + verify the agent, but do NOT
|
||||
# provision a guest (re-install/upgrade an agent on a host
|
||||
@@ -99,6 +106,8 @@ DATAVOL_GROW=""
|
||||
SYSDATA_GROW=""
|
||||
PASSPHRASE_FILE=""
|
||||
PRESERVE_FROM=""
|
||||
PRESERVE_STATE_FROM="" # dir holding a prior local-api.{crt,key} + local-tokens.log to carry over (keeps the pin stable across a reinstall)
|
||||
ALLOW_NEW_LEAF=false # opt-in to intentionally regenerate the agent leaf on a populated host (else the guard refuses)
|
||||
FORCE=false
|
||||
FORCE_GITEA_GOLDEN=false
|
||||
SKIP_PROVISION=false
|
||||
@@ -242,6 +251,8 @@ while [[ $# -gt 0 ]]; do
|
||||
--sysdata-grow) SYSDATA_GROW="$2"; shift 2 ;;
|
||||
--passphrase-file) PASSPHRASE_FILE="$2"; shift 2 ;;
|
||||
--preserve-from) PRESERVE_FROM="$2"; shift 2 ;;
|
||||
--preserve-state-from) PRESERVE_STATE_FROM="$2"; shift 2 ;;
|
||||
--allow-new-leaf) ALLOW_NEW_LEAF=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
--force-gitea-golden) FORCE_GITEA_GOLDEN=true; shift ;;
|
||||
--skip-provision) SKIP_PROVISION=true; shift ;;
|
||||
@@ -571,6 +582,44 @@ step_agent_install() {
|
||||
run chown -R "${AGENT_USER}:${AGENT_USER}" "$AGENT_STATE_DIR"
|
||||
run chmod 0750 "$AGENT_STATE_DIR"
|
||||
|
||||
# ── Agent local-API leaf lifecycle (B.2) ──────────────────────────────────────────────────────
|
||||
# The leaf's SHA-256 is pinned into EVERY guest's bootstrap. A reinstall that REGENERATES the leaf
|
||||
# invalidates every controller's pin (the 2026-06-28 root→non-root incident → controller↔agent dead
|
||||
# for days). Two protections:
|
||||
# (a) --preserve-state-from DIR: carry the prior leaf+key+token-store over → the fp stays STABLE,
|
||||
# no re-bootstrap needed. (Distinct from --preserve-from, which merges config sections only.)
|
||||
# (b) populated-host guard: REFUSE to proceed leaf-less on a host that already has guests, unless
|
||||
# --preserve-state-from or an explicit --allow-new-leaf is given. Converts the silent footgun
|
||||
# into a hard stop.
|
||||
local _have_leaf=false
|
||||
[[ -f "$AGENT_STATE_DIR/local-api.crt" && -f "$AGENT_STATE_DIR/local-api.key" ]] && _have_leaf=true
|
||||
|
||||
if [[ -n "$PRESERVE_STATE_FROM" ]]; then
|
||||
[[ -f "$PRESERVE_STATE_FROM/local-api.crt" && -f "$PRESERVE_STATE_FROM/local-api.key" ]] \
|
||||
|| die "--preserve-state-from $PRESERVE_STATE_FROM: local-api.crt/key not found there"
|
||||
openssl x509 -in "$PRESERVE_STATE_FROM/local-api.crt" -noout >/dev/null 2>&1 \
|
||||
|| die "--preserve-state-from: $PRESERVE_STATE_FROM/local-api.crt does not parse as an X.509 cert — refusing"
|
||||
if $DRY_RUN; then
|
||||
log_dry "preserve agent state: copy local-api.{crt,key}+local-tokens.log from $PRESERVE_STATE_FROM -> $AGENT_STATE_DIR (chown $AGENT_USER; 644/600/600)"
|
||||
else
|
||||
install -o "$AGENT_USER" -g "$AGENT_USER" -m 0644 "$PRESERVE_STATE_FROM/local-api.crt" "$AGENT_STATE_DIR/local-api.crt"
|
||||
install -o "$AGENT_USER" -g "$AGENT_USER" -m 0600 "$PRESERVE_STATE_FROM/local-api.key" "$AGENT_STATE_DIR/local-api.key"
|
||||
[[ -f "$PRESERVE_STATE_FROM/local-tokens.log" ]] && \
|
||||
install -o "$AGENT_USER" -g "$AGENT_USER" -m 0600 "$PRESERVE_STATE_FROM/local-tokens.log" "$AGENT_STATE_DIR/local-tokens.log"
|
||||
log_success " preserved agent leaf+token store from $PRESERVE_STATE_FROM (pin stays stable — no re-bootstrap)"
|
||||
fi
|
||||
_have_leaf=true
|
||||
fi
|
||||
|
||||
if ! $_have_leaf && ! $ALLOW_NEW_LEAF; then
|
||||
if pct list 2>/dev/null | tail -n +2 | grep -q .; then
|
||||
die "this host already has guests but $AGENT_STATE_DIR has no agent leaf to preserve.
|
||||
Re-running here will REGENERATE the leaf and invalidate every controller's pin (the 2026-06-28 incident).
|
||||
Pass --preserve-state-from <old state dir> to keep the pin stable, or --allow-new-leaf to regenerate
|
||||
intentionally (every guest must then be re-bootstrapped)."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sudoers — fetch the canonical file, validate with visudo -cf BEFORE installing (0440 root:root).
|
||||
if $DRY_RUN; then
|
||||
log_dry "fetch configs/felhom-agent.sudoers ; visudo -cf ; install 0440 -> $AGENT_SUDOERS"
|
||||
|
||||
Reference in New Issue
Block a user