feat(felhomsshd): dedicated OOB sshd instance + port-adaptive belt (H1 Parts 2-4 agent)

internal/felhomsshd: agent-managed felhom-sshd (claim port [8822,2222,8022,62222]
loud-fail-on-exhaustion; render config→sshd -t→reload never-restart-on-change
[SF-2]; operator authorized_keys from the hub block outside ~/.ssh [SF-3]); the
static-table nft belt mutating ONLY @operator_ips + @ssh_port [trap 4]; health/heal
(reset-failed-then-restart with 10min cooldown, NEVER restart onto an invalid
config) + the oob heartbeat stanza. configs/felhom-sshd.service (SAFE, no
RuntimeDirectory [SF-1]). FELHOM_SSHD + FELHOM_OOB sudoers (set-elements only).
oob.enabled config DEFAULT FALSE. Wired into main like wgtunnel.

Non-hollow tests: claim clean/contention/idempotent/exhaustion; config
safe+byte-stable+refuses-:22; belt mutate-then-idempotent + never-touches-rules;
heal no-restart-on-invalid-config + cooldown; status reflects block.

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 22:27:02 +02:00
parent effff53f99
commit c983a25609
14 changed files with 1140 additions and 6 deletions
+16
View File
@@ -70,6 +70,7 @@ type Collector struct {
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)
oob OOBReporter // H1: operator-access health (nil → stanza omitted)
hostID string
agentVersion string
logger *slog.Logger
@@ -154,6 +155,17 @@ func (c *Collector) SetMgmtPlaneReporter(m MgmtPlaneReporter) *Collector {
return c
}
// OOBReporter is the H1 seam the felhom-sshd loop plugs into (nil → no oob stanza).
type OOBReporter interface {
OOBStatus(ctx context.Context) *OOBStatus
}
// SetOOBReporter wires the operator-access health source (H1; nil-safe → stanza omitted).
func (c *Collector) SetOOBReporter(o OOBReporter) *Collector {
c.oob = o
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;
@@ -200,6 +212,10 @@ func (c *Collector) Collect(ctx context.Context) (*HostReport, error) {
if c.mgmtPlane != nil {
report.MgmtPlane = c.mgmtPlane.MgmtPlaneStatus(ctx)
}
// H1: operator-access (OOB) health (nil reporter = feature not wired → stanza omitted).
if c.oob != nil {
report.OOB = c.oob.OOBStatus(ctx)
}
return report, nil
}