The KEXINIT-reset diagnostic, the three-layer model, PVE-web-console recovery with the vaulted root@pam credential, the /run/sshd one-line fix, finding a recurring clobber, and the keep-the-180-token-fresh standing note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
5.9 KiB
RUNBOOK — Break-glass: management-plane recovery (privsep watchdog + vaulted console credential)
What this covers: how a Felhom host stays reachable when its SSH management plane breaks, and how the operator recovers if it does anyway. Delivered by TASK G1 (felhom-agent v0.71.0 + hub v0.34.0 + host-install). Design provenance:
documentation/audits/SPIKE-felhom-sshd-2026-07-05.md§8 (the incident) + #9 (the prerequisite). This is the blocker the felhom-sshd feature (H1) depends on — H1 may assume/run/sshdis guaranteed present and must never declareRuntimeDirectory=.
1. The failure (know the fingerprint)
/run/sshd is OpenSSH's compiled-in privilege-separation directory, SHARED by every sshd on the
host. If it goes missing, sshd's listener stays up (TCP connects, the version banner exchanges) but
every new session RESETS right after SSH2_MSG_KEXINIT. The box is healthy underneath (guests,
PVE web, agent all fine) but remotely unreachable over SSH.
Diagnostic (from any client):
ssh -vv root@<host> 2>&1 | grep -A1 KEXINIT
# ... "SSH2_MSG_KEXINIT sent" immediately followed by "Connection reset by <host> port 22" = /run/sshd missing
The spike caused this live via a second sshd's RuntimeDirectory=sshd cleanup removing the shared dir.
2. The three layers (defense in depth)
| Layer | Artifact | What it does | Failure domain it survives |
|---|---|---|---|
| 1 — prevent | /etc/tmpfiles.d/felhom-privsep.conf (d /run/sshd 0755 root root -) |
/run/sshd is boot-persistent, owned by no unit's lifecycle → the known cause can't recur |
reboots, unit-cleanup edges |
| 2 — auto-heal (no login) | felhom-mgmt-watchdog.timer → .service → /usr/local/sbin/felhom-mgmt-watchdog (~60s) |
recreates a missing /run/sshd, reset-faileds a start-limited sshd, writes a heal-marker. Agent-INDEPENDENT — heals with felhom-agent stopped |
a novel runtime cause; the agent being down |
| 3 — break-glass (human) | root@pam password, hub-vaulted at day-0 | operator reaches the PVE web console (pveproxy :8006 — a daemon/port/codebase distinct from sshd) and runs the one-line fix | both the sshd path AND the auto-heal failing |
Visibility: the agent's heartbeat carries a mgmt_plane stanza (privsep_dir_ok, sshd_reachable,
healed_recently, privsep_healed_at); the hub raises a mgmt_plane_healed warning on each new
auto-heal so a recurring clobber is investigated before it becomes a lockout.
3. Recovery — do this in order
3.0 First: is it even broken? (it usually self-heals)
Layer 2 heals within ~60s with nobody logged in. If you just saw the KEXINIT-reset, wait one minute and
retry SSH. If it's back, check the hub for a mgmt_plane_healed warning and go to §4 (find the cause).
3.1 If SSH is still dead — reach the PVE web console (break-glass)
- Retrieve the vaulted root@pam password (operator/global key):
(A per-host key is refused here by design — only the operator key reads a console password back.)
curl -s -H "Authorization: Bearer <GLOBAL-KEY out-of-band>" \ https://hub.felhom.eu/api/v1/admin/hosts/<host_id>/recovery-credential # → {"host_id":"…","username":"root@pam","password":"…","set_at":"…"} - Open the PVE web UI
https://<host-ip>:8006, log inroot@pam+ that password, realm Linux PAM standard authentication. - Node → Shell (xterm.js root console). Or navigate directly:
https://<host-ip>:8006/?console=shell&xtermjs=1&node=<node>&resize=off - Run the one-line fix:
SSH on :22 is restored instantly — no sshd restart needed (the listener never died; PID is unchanged). Verify:
mkdir -p /run/sshd && chmod 0755 /run/sshdls -ld /run/sshdand a freshssh root@<host>. - If the stock sshd is stuck
failedfrom a start-limit:systemctl reset-failed ssh && systemctl start ssh.
3.2 If you have ANY root shell on the box
Just run the §3.1 step 4 one-liner. That's the whole fix. The watchdog will also keep /run/sshd
present from then on (layer 2), and the tmpfiles entry restores it on the next boot (layer 1).
4. After recovery — find the recurring cause
A single auto-heal is noise; a repeating mgmt_plane_healed warning means something keeps removing
/run/sshd. Look for:
- a systemd unit declaring
RuntimeDirectory=sshd(the incident cause) —grep -rl RuntimeDirectory /etc/systemd/system /run/systemd; a second sshd unit must NEVER declare it (H1's constraint). - a cleanup/upgrade job that
rms/run/sshd. Fix the cause; the three layers are the safety net, not the cure.
5. Operator hygiene / standing notes
- Keep the build-server PVE token fresh. In the spike incident the build-server-cached PVE token
(
kisfenyo@192.168.0.180:~/felhom-agent-demo.json) was stale (401) and left no non-SSH lever before the web-console path was used. A working break-glass path independent of sshd must always exist. - Rotate the console credential with
felhom-host-install … --rotate-recovery(regenerates + re-vaults; a plain re-run does NOT rotate, to avoid stranding your saved copy). Full auto-rotation is a future item. - The password is never logged or filed — it exists only in the hub vault (retrieve as in §3.1).
- Least-privilege console user instead of root@pam was probed as a non-blocking observation: PVE
has historically gated the node Shell to
root@pam, so G1 ships the proven root@pam path. Revisit if PVE later allows a scoped realm user to open a node Shell.
6. Cross-references
documentation/architecture/06-offsite-connectivity.md(the OOB transport this complements).documentation/audits/SPIKE-felhom-sshd-2026-07-05.md§8/#9 (the incident + prerequisite).- felhom-agent
internal/mgmtplane/+configs/felhom-mgmt-watchdog.*+configs/felhom-privsep.tmpfiles. - hub
internal/monitor/host_mgmtplane.go+internal/store/host_recovery.go.