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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+29
-3
@@ -76,6 +76,26 @@ type HostReport struct {
|
||||
// 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"`
|
||||
|
||||
// OOB is the operator-access health stanza (TASK H1). It answers the operator's question — "can I
|
||||
// get into this box right now, and if not, why" — from the hub: felhom-sshd up + on which port,
|
||||
// locally reachable, the tunnel handshake age (the OOB path rides wg-felhom), whether the operator
|
||||
// peer/key is configured, and whether the config is invalid. The heartbeat reaches the hub over
|
||||
// HTTPS even when felhom-sshd or the tunnel is DOWN (channel independence). `omitempty`: absent
|
||||
// when the feature is not wired (pre-H1) — additive, no hub-schema change.
|
||||
OOB *OOBStatus `json:"oob,omitempty"`
|
||||
}
|
||||
|
||||
// OOBStatus is the per-heartbeat operator-access health (TASK H1). Carries no secret.
|
||||
type OOBStatus struct {
|
||||
FelhomSshdActive bool `json:"felhom_sshd_active"` // the felhom-sshd unit is active
|
||||
FelhomSshdPort int `json:"felhom_sshd_port"` // the claimed OOB port (0 = unclaimed)
|
||||
Reachable bool `json:"reachable"` // a local TCP dial to the OOB port succeeds
|
||||
ConfigInvalid bool `json:"config_invalid"` // `sshd -t` on the OOB config fails
|
||||
OperatorPeerConfigured bool `json:"operator_peer_configured"` // oob_peer_ip present in desired-state
|
||||
OperatorKeyConfigured bool `json:"operator_key_configured"` // operator authorized_key installed
|
||||
WGHandshakeAgeS *int64 `json:"wg_handshake_age_s,omitempty"` // wg-felhom last handshake age (nil = unknown)
|
||||
HealedAt string `json:"healed_at,omitempty"` // last felhom-sshd auto-heal (RFC3339)
|
||||
}
|
||||
|
||||
// MgmtPlaneStatus is the per-heartbeat management-plane health (TASK G1). Carries no secret.
|
||||
@@ -84,9 +104,9 @@ type HostReport struct {
|
||||
// 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)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -364,6 +384,12 @@ type WireWireguard struct {
|
||||
// set`) so it survives self-heal/restart/reboot ([OF-1]: a runtime widening was wiped by the
|
||||
// agent's own self-heal). `omitempty`: absent = OOB off, byte-identical conf for pre-H1 hosts.
|
||||
OOBPeerIP string `json:"oob_peer_ip,omitempty"`
|
||||
|
||||
// OOBOperatorSSHKey is the operator's SSH PUBLIC key (an authorized_keys line, TASK H1). The agent
|
||||
// writes it to felhom-sshd's dedicated AuthorizedKeysFile (/etc/felhom-sshd/authorized_keys/{root,
|
||||
// felhom-op}) — OUTSIDE ~/.ssh, so the customer's sshd never honours it [SF-3]. Hub-driven so it
|
||||
// rotates fleet-wide. `omitempty`: absent = no operator login installed. NOT a secret (public key).
|
||||
OOBOperatorSSHKey string `json:"oob_operator_ssh_key,omitempty"`
|
||||
}
|
||||
|
||||
// WireWireguardEndpoint is the endpoint half of the wireguard block.
|
||||
|
||||
Reference in New Issue
Block a user