05d81810d4
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
84 lines
2.7 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|