hub: F-14 part 2 — gated auto-Reissue on the PBS-DR enable dead-end

pbsdrProvisionAtom, on tenantsync token_exists: consult the customer's
MOST RECENT host-deletion record. escrow_acked=true -> invoke the
EXISTING tenantsync Reissue op, store the audit event (pbsdr_auto_reissue,
hub-source, "Previous key destroyed (acknowledged deletion) - credentials
re-issued automatically."), proceed to secret+descriptor as a normal
provision. No record / un-acked -> the pre-existing refusal, byte-unchanged
(never-silently-re-key law; operator ruling 2026-07-13).
Scenario A test drives the REAL handler path over a REAL escrow-ack delete;
scenario B pins the exact non-effect (zero Reissue calls, no state).
Red-proof: bypassing the gate flips both B subtests to 303 (silent re-key).
This commit is contained in:
2026-07-13 14:44:42 +02:00
parent 2321077800
commit 04861a7ed3
2 changed files with 146 additions and 6 deletions
+32 -5
View File
@@ -205,11 +205,38 @@ func (s *Server) pbsdrProvisionAtom(ctx context.Context, customerID string, host
defer cancel()
res, err := s.tenantsync.Provision(ctx, customerID)
if errors.Is(err, tenantsync.ErrTokenExists) {
// ep0 has a token but the hub has no descriptor — state mismatch (lost hub state or a
// half-torn earlier attempt). Never silently re-key: the operator decides via Re-issue.
return "", fmt.Errorf("the endpoint already holds a PBS token for %s but the hub has no descriptor — use the explicit \"Re-issue PBS credentials\" action", customerID)
}
if err != nil {
// ep0 has a token but the hub has no descriptor — state mismatch (lost hub state, a
// half-torn earlier attempt, or the F-14 shape: host deleted, tenancy survived).
//
// F-14 gate (operator ruling 2026-07-13): auto-re-issue is permitted ONLY when the
// hub's own deletion record shows the tenancy's owning host — the customer's most
// recent host deletion — was removed through the escrow-ack flow. Acknowledged
// destruction is not silent re-keying; the old secret went down with the acked host.
// No record / un-acked record → the refusal below, byte-unchanged (manual path).
rec, derr := s.store.LatestHostDeletion(customerID)
if derr != nil {
return "", fmt.Errorf("pbsdr: deletion-provenance lookup for %s: %w", customerID, derr)
}
if rec == nil || !rec.EscrowAcked {
return "", fmt.Errorf("the endpoint already holds a PBS token for %s but the hub has no descriptor — use the explicit \"Re-issue PBS credentials\" action", customerID)
}
res, err = s.tenantsync.Reissue(ctx, customerID) // the EXISTING re-issue op — no new endpoint interaction
if err != nil {
return "", fmt.Errorf("pbsdr: F-14 auto re-issue for %s: %w", customerID, err)
}
note := "Previous key destroyed (acknowledged deletion) — credentials re-issued automatically."
details, _ := json.Marshal(map[string]string{
"deleted_host": rec.HostID,
"deleted_at": rec.DeletedAt.UTC().Format(time.RFC3339),
"new_host": host.HostID,
"token_id": res.TokenID,
})
if _, eerr := s.store.SaveEvent(customerID, "pbsdr_auto_reissue", "info", note, string(details), "hub"); eerr != nil {
s.logger.Printf("[WARN] pbsdr: F-14 audit event for %s not stored: %v", customerID, eerr)
}
s.logger.Printf("[INFO] pbsdr F-14 auto re-issue for %s: owning host %s removed via escrow-ack flow (%s) — %s",
customerID, rec.HostID, rec.DeletedAt.UTC().Format(time.RFC3339), note)
} else if err != nil {
return "", err
}
+114 -1
View File
@@ -26,7 +26,8 @@ import (
type fakeTenancy struct {
provisionCalls int
reissueCalls int
err error
err error // both ops fail with this
provisionErr error // Provision-only failure (the F-14 token_exists shape: reissue still works)
secret string
}
@@ -45,6 +46,9 @@ func (f *fakeTenancy) Provision(ctx context.Context, customerID string) (*tenant
if f.err != nil {
return nil, f.err
}
if f.provisionErr != nil {
return nil, f.provisionErr
}
return f.result(customerID), nil
}
@@ -372,6 +376,115 @@ func TestPBSDR_StorageIDChangeUpdatesDescriptorOnly(t *testing.T) {
}
}
// F-14 scenario A (v0.53.0, operator ruling 2026-07-13): a tenancy orphaned by an ESCROW-ACKED
// host delete → the enable path auto-re-issues via the EXISTING tenantsync re-issue op, writes
// the audit event, and provisioning proceeds to a full descriptor + consume-once secret.
// RED-PROOF (Part 1): dropping the provenance write from DeleteHost's tx → the gate finds no
// record → this test fails with the 502 refusal.
func TestPBSDR_F14AutoReissueOnAckedDeletion(t *testing.T) {
fake := &fakeTenancy{provisionErr: tenantsync.ErrTokenExists, secret: "REISSUED-SECRET"}
s, st, logBuf := newPBSDRServer(t, fake)
// The F-14 history: the tenancy's owning host was deleted through the escrow-ack flow
// (real DeleteHost, real escrow row — no hand-set provenance).
if err := st.UpsertHost(&store.Host{HostID: "peti-00-dead", CustomerID: "peti", APIKey: "oldkey"}); err != nil {
t.Fatal(err)
}
if err := st.SaveHostEscrow("peti-00-dead", []byte("opaque"), "fp", "posture", "2026-07-01T00:00:00Z", ""); err != nil {
t.Fatal(err)
}
if err := st.DeleteHost("peti-00-dead", true); err != nil {
t.Fatalf("escrow-ack delete: %v", err)
}
rr := postUpdate(t, s, url.Values{"dr_tier": {"on"}})
if rr.Code != 303 {
t.Fatalf("enable with acked deletion record = %d (%s), want 303 (auto re-issue proceeds)", rr.Code, rr.Body.String())
}
if fake.provisionCalls != 1 || fake.reissueCalls != 1 {
t.Errorf("calls = provision %d / reissue %d, want 1/1 (the EXISTING re-issue op, once)", fake.provisionCalls, fake.reissueCalls)
}
// Full effect: descriptor landed, generation bumped, fresh consume-once secret staged.
desc, desiredJSON, gen := hostState(t, st)
if desc == nil || !desc.Enabled || desc.Namespace != "peti" || desc.TokenID != "felhom@pbs!peti" {
t.Fatalf("descriptor after auto re-issue = %+v (json %s)", desc, desiredJSON)
}
if gen != 1 {
t.Errorf("generation = %d, want 1", gen)
}
if got, err := st.ConsumeHostPBSSecret("peti-01"); err != nil || got != "REISSUED-SECRET" {
t.Fatalf("consume-once secret = (%q, %v), want the re-issued secret", got, err)
}
// The audit line: a stored hub-source event carrying the operator note.
events, err := st.GetRecentEvents("peti", 10)
if err != nil {
t.Fatalf("events: %v", err)
}
var audit *store.Event
for i := range events {
if events[i].EventType == "pbsdr_auto_reissue" {
audit = &events[i]
}
}
if audit == nil {
t.Fatal("no pbsdr_auto_reissue audit event stored")
}
if audit.Source != "hub" || !strings.Contains(audit.Message, "Previous key destroyed (acknowledged deletion)") {
t.Errorf("audit event = %+v, want hub-source with the operator note", audit)
}
if !strings.Contains(audit.DetailsJSON, "peti-00-dead") {
t.Errorf("audit details lack the deleted host: %s", audit.DetailsJSON)
}
if strings.Contains(logBuf.String(), "REISSUED-SECRET") {
t.Error("secret leaked into the hub log")
}
}
// F-14 scenario B (the never-silently-re-key law): a surviving tenancy WITHOUT an acked
// deletion record — none at all, or the latest one un-acked — keeps the current refusal and
// records ZERO re-issue calls (the exact non-effect).
// RED-PROOF (§10 B): removing the record check in pbsdrProvisionAtom (auto-reissue
// unconditionally) → the zero-reissue assertions here fail.
func TestPBSDR_F14NoRecordNeverRekeys(t *testing.T) {
assertRefused := func(t *testing.T, fake *fakeTenancy, s *Server, st *store.Store) {
t.Helper()
rr := postUpdate(t, s, url.Values{"dr_tier": {"on"}})
if rr.Code != 502 || !strings.Contains(rr.Body.String(), "Re-issue PBS credentials") {
t.Fatalf("save = %d (%s), want the byte-unchanged 502 refusal", rr.Code, rr.Body.String())
}
if fake.reissueCalls != 0 {
t.Errorf("reissue calls = %d, want 0 — SILENT RE-KEY", fake.reissueCalls)
}
if desc, _, gen := hostState(t, st); desc != nil || gen != 0 {
t.Errorf("state written on refusal: desc=%+v gen=%d", desc, gen)
}
if _, err := st.ConsumeHostPBSSecret("peti-01"); err != sql.ErrNoRows {
t.Errorf("a secret was staged on refusal (err %v)", err)
}
}
t.Run("no deletion record at all", func(t *testing.T) {
fake := &fakeTenancy{provisionErr: tenantsync.ErrTokenExists, secret: "S"}
s, st, _ := newPBSDRServer(t, fake)
assertRefused(t, fake, s, st)
})
t.Run("latest record un-acked", func(t *testing.T) {
fake := &fakeTenancy{provisionErr: tenantsync.ErrTokenExists, secret: "S"}
s, st, _ := newPBSDRServer(t, fake)
// A deletion happened, but NOT through the escrow-ack flow (no escrow row → acked=false).
if err := st.UpsertHost(&store.Host{HostID: "peti-00-dead", CustomerID: "peti", APIKey: "oldkey"}); err != nil {
t.Fatal(err)
}
if err := st.DeleteHost("peti-00-dead", true); err != nil {
t.Fatal(err)
}
assertRefused(t, fake, s, st)
})
}
func TestPBSDR_Reissue(t *testing.T) {
fake := &fakeTenancy{secret: "OLD-SECRET"}
s, st, logBuf := newPBSDRServer(t, fake)