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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user