feat(hub): v0.57.0 reinstall-of-existing-customer arc — claim/offsite/escrow continuity

F2 claim re-issue on clean-slate re-enroll (ReissueForReenroll, host-enroll mint path,
single-bump, reset code; hub never stores the password so fork B). F3 offsite re-issue on
re-enroll (ReissueOffsiteForCustomer, same machinery as the manual button). 2.3 escrow honesty
(red-proofed): re-issuing offsite marks the escrow stale (MarkEscrowStale), withholds the
mismatched restic hash from auto-confirm, DR checklist shows stale not done. Events:
claim_reissued_reenroll / offsite_reissued / escrow_stale.

Controller + scripts unchanged (source contradicted both premises): the controller reads escrow
prereqs live from the agent; the installer can't know the descriptor-provisioned storage id. F4
root fix is agent-side -> ROADMAP R-22; demo unblocked live (Part 0 ACL grant). VALIDATION doc
F2 erratum + F3/F4 dispositions. Green gate + Scenario-C red-proof pass.
This commit is contained in:
2026-07-16 18:00:13 +02:00
parent dd961a66bb
commit 7747a16ff1
15 changed files with 395 additions and 37 deletions
+30
View File
@@ -194,6 +194,36 @@ func (p *Provisioner) ReissueCredentials(ctx context.Context, customerID, typ st
if err := p.Store.SaveOneTimeSecret(customerID, pw); err != nil {
return fmt.Errorf("offsite: store re-issued one-time password: %w", err)
}
// v0.57.0 (2.3, the escrow-honesty fix): the restic repo password just changed, so any existing
// key-escrow blob — which sealed the OLD password — is now STALE. A recovery code minted against
// it would decrypt a password that no longer opens the repo. Mark the escrow stale so the hub
// stops advertising "ceremony done" and the customer's escrow wizard is offered again; a fresh
// ceremony seals the new password and clears the flag. Every credential change also emits a
// visible customer event (offsite_reissued always; escrow_stale only when a blob was invalidated).
// Best-effort: the password reset already succeeded — a bookkeeping failure here must not fail it.
escrowStaled := false
if host, herr := p.Store.GetHostByCustomer(customerID); herr == nil && host != nil {
if esc, eerr := p.Store.GetHostEscrow(host.HostID); eerr == nil && esc != nil {
if serr := p.Store.MarkEscrowStale(host.HostID); serr != nil {
p.logf("[offsite] WARN mark-escrow-stale for %s: %v", customerID, serr)
} else {
escrowStaled = true
}
}
}
if _, serr := p.Store.SaveEvent(customerID, "offsite_reissued", "info",
"Az offsite (házon kívüli) mentési hozzáférést újra kiadtuk — az új egyszeri jelszót a vezérlő a következő frissítéskor átveszi.",
"", "hub"); serr != nil {
p.logf("[offsite] WARN save offsite_reissued event for %s: %v", customerID, serr)
}
if escrowStaled {
if _, serr := p.Store.SaveEvent(customerID, "escrow_stale", "warning",
"A helyreállítási kulcs-letét elavult az offsite jelszó cseréje miatt — futtasd le újra a helyreállítási szertartást (Biztonsági mentés → Helyreállítás).",
"", "hub"); serr != nil {
p.logf("[offsite] WARN save escrow_stale event for %s: %v", customerID, serr)
}
}
return nil
}