Files
felhom.eu/hub/internal/store/host_recovery_test.go
T
admin 05d81810d4 feat(hub,install): break-glass recovery vault + mgmt_plane surfacing (TASK G1)
Hub half of the management-plane break-glass (prereq for felhom-sshd/H1; agent
half = felhom-agent v0.71.0). Closes SPIKE-felhom-sshd §8/#9.

- store.host_recovery + methods: per-host root@pam console password, at-rest,
  operator-retrievable (the PVE-web-console fallback when sshd + auto-heal both fail).
- API: PUT /hosts/{id}/recovery-credential (self-scoped, day-0 vaults) + GET
  /admin/hosts/{id}/recovery-credential (global key only). Secret never logged
  (red-proofed).
- monitor/host_mgmtplane: parses the agent mgmt_plane stanza, raises
  mgmt_plane_healed WARNING on a new privsep_healed_at (recurring clobber surfaces
  before lockout; complements host_staleness).
- host-install: step_break_glass generates a strong root@pam password (openssl
  rand, never logged/filed — stdin to chpasswd + curl), vaults via host key;
  idempotent unless --rotate-recovery. Installs the G1 host artifacts (tmpfiles +
  agent-independent watchdog timer), RuntimeDirectory-guarded; uninstall removes them.

Hub v0.34.0. Non-hollow tests + red-proofs; full suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-05 19:03:18 +02:00

84 lines
2.7 KiB
Go

package store
import "testing"
func TestHostRecoveryCredential_RoundTripUpsertAndAbsent(t *testing.T) {
s := newTestStore(t)
if err := s.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"}); err != nil {
t.Fatalf("UpsertHost: %v", err)
}
// absent → (nil, nil) + Has=false
got, err := s.GetHostRecoveryCredential("h1")
if err != nil || got != nil {
t.Fatalf("absent cred: got %+v / %v (want nil,nil)", got, err)
}
has, _ := s.HasHostRecoveryCredential("h1")
if has {
t.Fatal("HasHostRecoveryCredential must be false before any vault")
}
// vault → round-trips
if err := s.SaveHostRecoveryCredential("h1", "root@pam", "s3cret-Aa1"); err != nil {
t.Fatalf("SaveHostRecoveryCredential: %v", err)
}
got, err = s.GetHostRecoveryCredential("h1")
if err != nil || got == nil {
t.Fatalf("GetHostRecoveryCredential: %+v / %v", got, err)
}
if got.Username != "root@pam" || got.Secret != "s3cret-Aa1" {
t.Fatalf("round-trip mismatch: %+v", got)
}
if has, _ := s.HasHostRecoveryCredential("h1"); !has {
t.Fatal("HasHostRecoveryCredential must be true after vault")
}
// upsert (rotate) → overwrites last-write-wins
if err := s.SaveHostRecoveryCredential("h1", "root@pam", "rotated-Bb2"); err != nil {
t.Fatalf("re-vault: %v", err)
}
got, _ = s.GetHostRecoveryCredential("h1")
if got.Secret != "rotated-Bb2" {
t.Fatalf("rotate did not overwrite: %+v", got)
}
}
func TestGetHostMgmtPlaneStates_ParsesHealMarker(t *testing.T) {
s := newTestStore(t)
if err := s.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"}); err != nil {
t.Fatalf("UpsertHost: %v", err)
}
// a report WITH a heal marker
report := `{"host_id":"h1","mgmt_plane":{"privsep_dir_ok":true,"sshd_reachable":true,"healed_recently":true,"privsep_healed_at":"2026-07-05T16:42:17Z"}}`
if err := s.SaveHostReport("h1", "c1", []byte(report), HostReportDenorm{}); err != nil {
t.Fatalf("SaveHostReport: %v", err)
}
rows, err := s.GetHostMgmtPlaneStates()
if err != nil {
t.Fatalf("GetHostMgmtPlaneStates: %v", err)
}
var found bool
for _, r := range rows {
if r.HostID == "h1" {
found = true
if !r.PrivsepDirOK || r.PrivsepHealedAt != "2026-07-05T16:42:17Z" {
t.Fatalf("parsed row wrong: %+v", r)
}
}
}
if !found {
t.Fatal("h1 not in mgmt-plane states")
}
// a report WITHOUT the stanza (old agent) → zero values, no crash
if err := s.SaveHostReport("h1", "c1", []byte(`{"host_id":"h1"}`), HostReportDenorm{}); err != nil {
t.Fatalf("SaveHostReport2: %v", err)
}
rows, _ = s.GetHostMgmtPlaneStates()
for _, r := range rows {
if r.HostID == "h1" && r.PrivsepHealedAt != "" {
t.Fatalf("old-agent report should yield empty healed_at, got %+v", r)
}
}
}