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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeMgmtPlane is a MgmtPlaneReporter returning a fixed stanza (or nil).
|
||||
type fakeMgmtPlane struct{ st *MgmtPlaneStatus }
|
||||
|
||||
func (f fakeMgmtPlane) MgmtPlaneStatus(context.Context) *MgmtPlaneStatus { return f.st }
|
||||
|
||||
func TestCollect_MgmtPlaneOmittedWhenReporterNil(t *testing.T) {
|
||||
px := &fakePx{node: "n", ns: newTestNodeStatus()}
|
||||
c := NewCollector(px, fakeProber{status: "active"}, fakeObserver{}, nil, nil, nil, "h", "0.71.0", quietLogger())
|
||||
r, err := c.Collect(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Collect: %v", err)
|
||||
}
|
||||
if r.MgmtPlane != nil {
|
||||
t.Fatalf("no reporter wired → mgmt_plane must be omitted (nil), got %+v", r.MgmtPlane)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollect_MgmtPlanePopulatedWhenWired(t *testing.T) {
|
||||
px := &fakePx{node: "n", ns: newTestNodeStatus()}
|
||||
c := NewCollector(px, fakeProber{status: "active"}, fakeObserver{}, nil, nil, nil, "h", "0.71.0", quietLogger())
|
||||
c.SetMgmtPlaneReporter(fakeMgmtPlane{st: &MgmtPlaneStatus{
|
||||
PrivsepDirOK: true, SshdReachable: true, HealedRecently: true, PrivsepHealedAt: "2026-07-05T16:42:17Z",
|
||||
}})
|
||||
r, err := c.Collect(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Collect: %v", err)
|
||||
}
|
||||
if r.MgmtPlane == nil {
|
||||
t.Fatal("wired reporter → mgmt_plane must be present")
|
||||
}
|
||||
if !r.MgmtPlane.HealedRecently || r.MgmtPlane.PrivsepHealedAt != "2026-07-05T16:42:17Z" {
|
||||
t.Fatalf("mgmt_plane not carried through: %+v", r.MgmtPlane)
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,28 @@ type HostReport struct {
|
||||
// pending=false, the correct default.
|
||||
SelfUpdatePending bool `json:"selfupdate_pending,omitempty"`
|
||||
SelfUpdatePendingVersion string `json:"selfupdate_pending_version,omitempty"`
|
||||
|
||||
// MgmtPlane is the management-plane health stanza (TASK G1). It reports whether OpenSSH's SHARED
|
||||
// privilege-separation dir (/run/sshd) is present and whether the stock sshd listener answers, plus
|
||||
// whether the agent-independent watchdog auto-healed a missing dir since boot (and when). This is
|
||||
// the visibility half of the break-glass system: the dumb watchdog fixes /run/sshd with no login,
|
||||
// and this stanza surfaces a RECURRING clobber to the operator BEFORE it becomes a lockout —
|
||||
// complementing host_staleness (which only catches a box gone silent). `omitempty` (the
|
||||
// SelfUpdatePending precedent): stored opaquely hub-side, so these additive fields need no
|
||||
// hub-schema change and are absent when the reporter is not wired.
|
||||
MgmtPlane *MgmtPlaneStatus `json:"mgmt_plane,omitempty"`
|
||||
}
|
||||
|
||||
// MgmtPlaneStatus is the per-heartbeat management-plane health (TASK G1). Carries no secret.
|
||||
// HealedRecently is true while the watchdog's heal-marker exists (a privsep-dir heal happened this
|
||||
// boot); PrivsepHealedAt is that marker's RFC3339 timestamp (absent when no heal has occurred). The
|
||||
// hub raises a warning event on a PrivsepHealedAt it has not alerted on — a recurring auto-heal means
|
||||
// a persistent clobber cause worth investigating before the box locks out.
|
||||
type MgmtPlaneStatus struct {
|
||||
PrivsepDirOK bool `json:"privsep_dir_ok"` // /run/sshd exists (the KEXINIT-reset detector)
|
||||
SshdReachable bool `json:"sshd_reachable"` // the stock sshd listener accepts TCP
|
||||
HealedRecently bool `json:"healed_recently"` // the watchdog heal-marker is present (this boot)
|
||||
PrivsepHealedAt string `json:"privsep_healed_at,omitempty"` // marker timestamp; hub warns on a NEW value
|
||||
}
|
||||
|
||||
// WireguardStatus is the per-heartbeat offsite-tunnel status (S3). LastHandshakeAgeS is nil when
|
||||
|
||||
Reference in New Issue
Block a user