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)
|
# (default: secure no-echo prompt)
|
||||||
# --preserve-from PATH merge non-Day-0 sections (privileged/storage/backup/
|
# --preserve-from PATH merge non-Day-0 sections (privileged/storage/backup/
|
||||||
# local_api/authz/lan_resolver) from an existing config
|
# 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)
|
# --force allow provisioning over an EXISTING vmid (destructive)
|
||||||
# --skip-provision install + configure + verify the agent, but do NOT
|
# --skip-provision install + configure + verify the agent, but do NOT
|
||||||
# provision a guest (re-install/upgrade an agent on a host
|
# provision a guest (re-install/upgrade an agent on a host
|
||||||
@@ -99,6 +106,8 @@ DATAVOL_GROW=""
|
|||||||
SYSDATA_GROW=""
|
SYSDATA_GROW=""
|
||||||
PASSPHRASE_FILE=""
|
PASSPHRASE_FILE=""
|
||||||
PRESERVE_FROM=""
|
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=false
|
||||||
FORCE_GITEA_GOLDEN=false
|
FORCE_GITEA_GOLDEN=false
|
||||||
SKIP_PROVISION=false
|
SKIP_PROVISION=false
|
||||||
@@ -242,6 +251,8 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--sysdata-grow) SYSDATA_GROW="$2"; shift 2 ;;
|
--sysdata-grow) SYSDATA_GROW="$2"; shift 2 ;;
|
||||||
--passphrase-file) PASSPHRASE_FILE="$2"; shift 2 ;;
|
--passphrase-file) PASSPHRASE_FILE="$2"; shift 2 ;;
|
||||||
--preserve-from) PRESERVE_FROM="$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) FORCE=true; shift ;;
|
||||||
--force-gitea-golden) FORCE_GITEA_GOLDEN=true; shift ;;
|
--force-gitea-golden) FORCE_GITEA_GOLDEN=true; shift ;;
|
||||||
--skip-provision) SKIP_PROVISION=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 chown -R "${AGENT_USER}:${AGENT_USER}" "$AGENT_STATE_DIR"
|
||||||
run chmod 0750 "$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).
|
# Sudoers — fetch the canonical file, validate with visudo -cf BEFORE installing (0440 root:root).
|
||||||
if $DRY_RUN; then
|
if $DRY_RUN; then
|
||||||
log_dry "fetch configs/felhom-agent.sudoers ; visudo -cf ; install 0440 -> $AGENT_SUDOERS"
|
log_dry "fetch configs/felhom-agent.sudoers ; visudo -cf ; install 0440 -> $AGENT_SUDOERS"
|
||||||
|
|||||||
Reference in New Issue
Block a user