R-275, R-276, R-881: the uninstall removes every copy of the agent config, the WireGuard tunnel and felhom-priv-apply

- R-275: the agent's own config dir is purged as a directory (the .bak* glob missed all five
  demo-hp copies); a custom config path loses agent.json + every agent.json.* sibling only.
  Sudoers dotted copies go too. At install, a freshly created service user cannot read what an old
  install left in /etc/felhom-agent (sealed root 0600, named, never deleted). vmbr9 and the ISO
  first-boot files are now NAMED under KEPT.
- R-276: wg-quick@wg-felhom is disabled --now and its conf removed, then observed down; the hub-side
  peer is named under KEPT (removing it is the hub's job).
- R-881: /usr/local/sbin/felhom-priv-apply is removed; the disclosure says it and the guest hook
  come from the config bundle.
- scripts/test_hostinstall.py: lifts the functions verbatim and runs them with PATH stubs (13 tests,
  BusyBox-safe); harness GL4-D here-string (SIGPIPE false miss), GL8-F1 follows the new purge.
- SCRIPT_VERSION 1.32.0 (not published; no tag).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 21:14:19 +02:00
parent c8da8e0439
commit 85de3f9b87
3 changed files with 487 additions and 28 deletions
+128 -8
View File
@@ -184,7 +184,7 @@
set -euo pipefail
SCRIPT_VERSION="1.31.0" # the SINGLE version source (F-1): -h and the run banners follow it.
SCRIPT_VERSION="1.32.0" # the SINGLE version source (F-1): -h and the run banners follow it.
# The hub used to carry a copy for its Setup tab; R-94 DELETED it
# (2026-08-02) because the hub cannot know which version a box runs —
# the Setup command fetches this script at run time. scripts/
@@ -811,9 +811,18 @@ _uninstall_statement() {
echo " WIPED (this run):"
echo " - guest $vmid (container + its OS/Docker/user-data volumes)"
if [[ "$scope" == "full" ]]; then
echo " - the felhom-agent: binary, unit, sudoers, config (+ its .bak backups), state dir, service user"
echo " - the felhom-agent: binary, unit, sudoers (+ its copies), state dir, service user, and its config"
echo " directory with every copy of the config in it"
if $_WG_PRESENT; then
if [[ -n "$_WG_TEARDOWN_NOTE" ]]; then
echo " - the WireGuard tunnel to the Felhom off-site endpoint: NOT shown down — $_WG_TEARDOWN_NOTE"
else
echo " - the WireGuard tunnel to the Felhom off-site endpoint (${WG_UNIT} disabled, ${WG_CONF} removed)"
fi
fi
echo " - self-update artifacts: guarded wrapper, A/B slots (.prev/.new.*), rollback unit, start-limit drop-in"
echo " - break-glass watchdog + OOB artifacts (where present); guest-hook snippet; dnsmasq snippets; the mkfs + pbs-apply wrappers"
echo " - break-glass watchdog + OOB artifacts (where present); guest-hook snippet; dnsmasq snippets; the mkfs, pbs-apply,"
echo " backup-target-apply, os-apply and priv-apply wrappers; the crash guard; the config-bundle record"
echo " - pveum: the Felhom roles/user/token/scoped ACL$( $pool_removed && printf '; the emptied %s pool' "$PVE_POOL")"
echo " - the install state file"
if $REMOVE_GOLDEN; then echo " - the golden vzdump (--remove-golden)"; fi
@@ -835,6 +844,21 @@ _uninstall_statement() {
echo " - the PBS backups + this customer's namespace on the PBS side — delete there if wanted"
fi
echo " - the hub host/customer record + report history (operator UI / DB)"
if [[ "$scope" == "full" ]] && $_WG_PRESENT; then
echo " - this host's WireGuard PEER on the hub and the off-site endpoint (its /32 and public key) — the"
echo " tunnel is down from this side; delete the peer in the hub (operator) if the customer is leaving"
fi
if [[ "$scope" == "full" ]]; then
# R-275: named, not removed — a host network change and the ISO's own first-boot files.
if grep -qE "^[[:space:]]*iface[[:space:]]+${ISLAND_BRIDGE}[[:space:]]" /etc/network/interfaces 2>/dev/null; then
echo " - the ${ISLAND_BRIDGE} island bridge stanza in /etc/network/interfaces (host-internal, no port; a reinstall"
echo " reuses it) — to remove it: delete the stanza, then ifreload -a"
fi
if [[ -e /etc/felhom/.bootstrap-done || -e /etc/felhom/appliance-pairing-code ]]; then
echo " - the appliance ISO's first-boot files: /etc/felhom/.bootstrap-done, /etc/felhom/appliance-pairing-code,"
echo " felhom-bootstrap.service (disabled, fired once) — not this script's; remove by hand if wanted"
fi
fi
echo " - the escrow blob in the hub, if one exists (operator UI)"
if $_had_break_glass; then
echo " - the hub-vaulted root@pam recovery credential — the box KEEPS the password step 4b set; rotate it if the box leaves Felhom management"
@@ -931,6 +955,90 @@ _dnsmasq_purge_owned() {
return 0
}
# _purge_agent_config CFG — remove the agent config AND every copy of it (R-275).
#
# The config holds the per-host hub api_key and the Proxmox token. Copies of it are made by hand and
# by tools, under names nobody can predict (`agent.json.campaign9-before`, `agent.json.pre-prunegate.bak`
# were measured on demo-hp 2026-08-09). The old `${cfg}.bak*` glob missed all five of them, and the
# reinstall then handed them to the new service account (same uid). So: the agent's OWN directory is
# purged as a directory. A config at a custom path (operator-chosen, maybe a shared dir) is never
# purged by directory — there the config and every `${cfg}.*` sibling go, then an empty dir.
# Pinned by scripts/test_hostinstall.py (test_purge_*).
AGENT_CFG_DIR_DEFAULT="/etc/felhom-agent"
_purge_agent_config() {
local cfg="$1" dir f
dir=$(dirname "$cfg")
if [[ "$dir" == "$AGENT_CFG_DIR_DEFAULT" ]]; then
if [[ -d "$dir" ]]; then
run rm -rf "$dir"
log_success " removed $dir (the agent config and every copy of it)"
else
log_skip " $dir already absent"
fi
return 0
fi
if [[ -f "$cfg" ]]; then run rm -f "$cfg"; else log_skip " $cfg already absent"; fi
for f in "${cfg}".*; do [[ -e "$f" ]] && run rm -f "$f"; done
run rmdir "$dir" 2>/dev/null || true
return 0
}
# _seal_old_agent_config DIR — at install, when the service user was JUST created (R-275, second half).
# A new system account can get the uid the deleted one had, so files a previous install left in the
# config dir would become readable by the new account. They are made root-only (0600 root:root) and
# named — never deleted (they may be an operator's own backup). Step 6 rewrites agent.json and gives
# it to the agent again, so only the old copies stay sealed.
_seal_old_agent_config() {
local dir="$1" f found=false
[[ -d "$dir" ]] || return 0
for f in "$dir"/* "$dir"/.[!.]*; do
[[ -f "$f" ]] || continue
found=true
run chown root:root "$f"
run chmod 0600 "$f"
log_warn " left by a previous install, now root-only: $f"
done
$found && log_warn " remove these by hand if you do not need them (they may hold an old hub key and Proxmox token)"
return 0
}
# _teardown_wg_tunnel — stop the WireGuard tunnel to the Felhom off-site endpoint (R-276).
#
# The agent creates it at run time (wg_tunnel.enabled is the default, decision 5) and the uninstall
# used to leave it enabled and handshaking: a box told to leave Felhom kept a live network path into
# Felhom's endpoint. Stop + disable the unit, remove its conf, then OBSERVE that it is down. The
# peer on the hub/endpoint side is not this script's to remove — the closing statement names it.
# Sets _WG_TEARDOWN_NOTE when the tunnel could not be shown down. Pinned by
# scripts/test_hostinstall.py (test_wg_*).
WG_UNIT="wg-quick@wg-felhom"
WG_CONF="/etc/wireguard/wg-felhom.conf"
_WG_TEARDOWN_NOTE=""
_WG_PRESENT=false
_teardown_wg_tunnel() {
local active=false enabled=false
systemctl is-active --quiet "$WG_UNIT" 2>/dev/null && active=true
systemctl is-enabled --quiet "$WG_UNIT" 2>/dev/null && enabled=true
if $active || $enabled || [[ -e "$WG_CONF" ]]; then _WG_PRESENT=true; fi
if ! $_WG_PRESENT; then
log_skip " WireGuard tunnel ($WG_UNIT) not present"
return 0
fi
if $active || $enabled; then
run systemctl disable --now "$WG_UNIT" || log_warn " systemctl disable --now $WG_UNIT returned non-zero"
fi
run systemctl reset-failed "$WG_UNIT" 2>/dev/null || true
if [[ -e "$WG_CONF" ]]; then run rm -f "$WG_CONF"; fi
$DRY_RUN && return 0
# Positive observable: the unit must now read inactive (an exit code is not an observation).
if systemctl is-active --quiet "$WG_UNIT" 2>/dev/null; then
_WG_TEARDOWN_NOTE="$WG_UNIT is STILL active after disable --now"
log_warn " $_WG_TEARDOWN_NOTE — stop it by hand: systemctl disable --now $WG_UNIT"
else
log_success " WireGuard tunnel to the Felhom off-site endpoint is down ($WG_UNIT disabled, conf removed)"
fi
return 0
}
run_uninstall() {
log_step "UNINSTALL — local host teardown"
@@ -1045,6 +1153,8 @@ run_uninstall() {
for bak in "${AGENT_UNIT}".bak-*; do [[ -e "$bak" ]] && run rm -f "$bak"; done
run systemctl daemon-reload
if [[ -f "$AGENT_SUDOERS" ]]; then run rm -f "$AGENT_SUDOERS"; else log_skip " $AGENT_SUDOERS already absent"; fi
# R-275: dotted copies (`felhom-agent.bak-pre-e2a`) are inert for sudo but are still a copy of it.
for bak in "${AGENT_SUDOERS}".*; do [[ -e "$bak" ]] && run rm -f "$bak"; done
if [[ -f "$AGENT_BIN" ]]; then run rm -f "$AGENT_BIN"; else log_skip " $AGENT_BIN already absent"; fi
for bak in "${AGENT_BIN}".bak-*; do [[ -e "$bak" ]] && run rm -f "$bak"; done
if [[ -d "$AGENT_STATE_DIR" ]]; then run rm -rf "$AGENT_STATE_DIR"; else log_skip " $AGENT_STATE_DIR already absent"; fi
@@ -1054,10 +1164,13 @@ run_uninstall() {
# drill R1 / GL-6 F1). The config write leaves `${agent_cfg}.bak*` siblings (e.g. .bak-<ver>,
# .bak-ceremony-*, .bak-pre064) — one GL-6 residue still held a LIVE hub api_key. Remove the
# config AND every `.bak*` sibling, then the (now-empty) dir. Paths logged, contents never.
if [[ -f "$agent_cfg" ]]; then run rm -f "$agent_cfg"; else log_skip " $agent_cfg already absent"; fi
local _cfgbak
for _cfgbak in "${agent_cfg}".bak*; do [[ -e "$_cfgbak" ]] && run rm -f "$_cfgbak"; done
run rmdir "$(dirname "$agent_cfg")" 2>/dev/null || true
# R-275: the `.bak*` glob missed every hand-made copy (`agent.json.campaign9-before` …), so the
# agent's own directory is now purged as a directory — see _purge_agent_config.
_purge_agent_config "$agent_cfg"
# 4b0. The WireGuard tunnel to the Felhom off-site endpoint (R-276). The agent is stopped above, so
# nothing re-enables it while it goes.
_teardown_wg_tunnel
# 4b2. Management-plane break-glass (TASK G1): timer+oneshot+script+tmpfiles. Stop/disable the
# timer, remove all four artifacts + the runtime heal-marker. We do NOT `rmdir /run/sshd` —
@@ -1161,6 +1274,8 @@ run_uninstall() {
if [[ -f /usr/local/sbin/felhom-pbs-apply ]]; then run rm -f /usr/local/sbin/felhom-pbs-apply; else log_skip " felhom-pbs-apply already absent"; fi
if [[ -f /usr/local/sbin/felhom-backup-target-apply ]]; then run rm -f /usr/local/sbin/felhom-backup-target-apply; else log_skip " felhom-backup-target-apply already absent"; fi
if [[ -f /usr/local/sbin/felhom-os-apply ]]; then run rm -f /usr/local/sbin/felhom-os-apply; else log_skip " felhom-os-apply already absent"; fi
# R-881: the content checker the config bundle installs since agent v0.146.1 (R-861).
if [[ -f /usr/local/sbin/felhom-priv-apply ]]; then run rm -f /usr/local/sbin/felhom-priv-apply; else log_skip " felhom-priv-apply already absent"; fi
# 1.30.0: the crash guard (kernel.panic goes back to the kernel default 0 at the next boot) and the root-owned
# slow-lane trust files.
if systemctl list-unit-files felhom-crash-guard.service >/dev/null 2>&1; then
@@ -1676,7 +1791,8 @@ _byo_disclosure_ack() {
+ /usr/local/sbin/felhom-pbs-apply (PBS-DR apply wrapper — DR capability is baked
on every install; ACTIVATION stays a hub flag, off = zero effect on this host)
+ felhom-mgmt-watchdog service+timer+script + /etc/tmpfiles.d/felhom-privsep.conf
+ guest-hook snippet under /var/lib/vz/snippets/ (agent-installed at runtime)
+ /usr/local/sbin/felhom-priv-apply + the guest-hook snippet under /var/lib/vz/snippets/
(both from the agent's config bundle)
+ the 'sudo' and 'age' packages if absent + install state dir ${STATE_DIR}
wg: an OUTBOUND WireGuard tunnel to the Felhom hub (wg_tunnel.enabled=true — base
infrastructure like the cloudflared tunnel; hands-free peer registration; the
@@ -2270,6 +2386,10 @@ step_agent_install() {
else
useradd --system --no-create-home --shell /usr/sbin/nologin "$AGENT_USER"
log_success " created service user $AGENT_USER"
# R-275: the new account may get the uid of a deleted one — make what an old install left
# in the agent's own config dir root-only. Only that dir: a custom config path may share a
# directory with files that are not ours.
_seal_old_agent_config "$AGENT_CFG_DIR_DEFAULT"
fi
# systemd-journal group: the NAS verify pipeline (agent v0.81.0) classifies mount failures from