feat(mgmtplane): break-glass privsep-dir watchdog + mgmt_plane health (TASK G1) — v0.71.0

Prerequisite for felhom-sshd (H1). Closes the SPIKE-felhom-sshd §8 lockout: a
second sshd's RuntimeDirectory=sshd removed the SHARED /run/sshd privsep dir and
took stock sshd on :22 down (sessions reset after KEXINIT).

Host artifacts (configs/, installed by felhom-host-install):
- felhom-privsep.tmpfiles: layer 1, boot-persistent /run/sshd owned by no unit
- felhom-mgmt-watchdog.sh/.service/.timer: layer 2, AGENT-INDEPENDENT ~60s heal
  (stat-first recreate + reset-failed sshd only if failed + heal-marker); never
  RuntimeDirectory=, never restarts stock sshd, never touches a healthy dir.

Go (internal/mgmtplane): read-only Reporter → additive omitempty mgmt_plane
heartbeat stanza (privsep_dir_ok/sshd_reachable/healed_recently/privsep_healed_at),
wired via Collector.SetMgmtPlaneReporter. Non-hollow tests + red-proofs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-05 18:49:27 +02:00
parent 1c75a45a42
commit fd4e177216
11 changed files with 459 additions and 1 deletions
+18
View File
@@ -69,6 +69,7 @@ type Collector struct {
leafFP string // v0.48.0: served local-API leaf fp (static per process; "" when local API disabled)
wg WireguardReporter // S3: offsite-tunnel status (nil → stanza omitted)
selfUpdate SelfUpdateReporter // D1: agent self-update pending status (nil → false)
mgmtPlane MgmtPlaneReporter // G1: management-plane health (nil → stanza omitted)
hostID string
agentVersion string
logger *slog.Logger
@@ -140,6 +141,19 @@ func (c *Collector) SetSelfUpdateReporter(s SelfUpdateReporter) *Collector {
return c
}
// MgmtPlaneReporter is the G1 seam the mgmtplane observer plugs into (same consumer-side pattern —
// hub does not import mgmtplane). nil (feature not wired) → no mgmt_plane stanza on the report.
type MgmtPlaneReporter interface {
MgmtPlaneStatus(ctx context.Context) *MgmtPlaneStatus
}
// SetMgmtPlaneReporter wires the management-plane health source (G1; nil-safe → stanza omitted).
// Returns the collector for chaining.
func (c *Collector) SetMgmtPlaneReporter(m MgmtPlaneReporter) *Collector {
c.mgmtPlane = m
return c
}
// Collect builds the report. Best-effort liveness: a failed NodeStatus is a hard
// error (no useful report — the cycle skips the POST); a failed per-guest
// GuestConfig degrades that guest to status="unknown" without spec but still sends;
@@ -182,6 +196,10 @@ func (c *Collector) Collect(ctx context.Context) (*HostReport, error) {
if c.selfUpdate != nil {
report.SelfUpdatePending, report.SelfUpdatePendingVersion = c.selfUpdate.SelfUpdatePending()
}
// G1: management-plane health (nil reporter = feature not wired → stanza omitted).
if c.mgmtPlane != nil {
report.MgmtPlane = c.mgmtPlane.MgmtPlaneStatus(ctx)
}
return report, nil
}