v0.77.0: fork-4 — escrow the offsite restic repo password under R

IdentityBundle gains ResticRepoPassword (rides existing age-under-R
WrapIdentityBundle; custody spike febdc56 proved a recovered value opens the
real repo). POST /escrow/stage-secret (withGuest, scopedFromBody) transiently
stages the controller-pushed password (0600, atomic, NEVER logged), which the
escrow-create ceremony auto-injects then wipes. Adds AttachResticPassword +
StagedResticPasswordPath + WipeStagedResticPassword; EscrowStagePath injectable
for tests. Tests: bundle carries pw byte-exact + not-in-blob + wrong-R fails
closed; stage 0600 + non-secret ack + cross-guest 403 + value-not-in-log.
Additive; PBS-K escrow untouched. NOT yet live-validated (supervised ceremony).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-09 14:56:09 +02:00
parent 7f07393623
commit 0c22b9bbf3
8 changed files with 320 additions and 1 deletions
+27
View File
@@ -1710,6 +1710,26 @@ func runSelftestEscrowCreate(ctx context.Context, cfg config.Config, logger *slo
logger.Info("escrow: identity bundle: +wg_private_key")
}
}
// fork-4: auto-inject the staged offsite restic repo password (controller-pushed via the local API)
// into the escrowed identity, so DR can recover the offsite DATA key with the one recovery code R.
// Field NAME only in logs. No staged file → clean no-attach (pre-fork-4 behavior). Wiped after create.
resticStaged := false
{
probe := identity
if probe == nil {
probe = &escrow.IdentityBundle{}
}
attached, err := escrow.AttachResticPassword(probe, escrow.StagedResticPasswordPath())
if err != nil {
fmt.Fprintf(os.Stderr, "selftest=escrow-create: %v\n", err)
return 1
}
if attached {
identity = probe
resticStaged = true
logger.Info("escrow: identity bundle: +restic_repo_password")
}
}
fmt.Printf("=== felhom-agent %s selftest=escrow-create (storage=%s posture=%s identity=%v) ===\n", version, storage, escrow.DefaultPosture, identity != nil)
// NB: nothing about R is logged. The logger never sees R; only stdout does, once.
@@ -1726,6 +1746,13 @@ func runSelftestEscrowCreate(ctx context.Context, cfg config.Config, logger *slo
fmt.Fprintln(os.Stderr, " [FAIL] escrow create:", err)
return 1
}
// fork-4: the staged restic password is now sealed inside the R-wrapped blob — wipe the transient
// 0600 staging file so it never lingers on disk (field name only; a wipe failure is a loud warn).
if resticStaged {
if werr := escrow.WipeStagedResticPassword(); werr != nil {
logger.Warn("escrow: could not wipe the staged restic password after create", "err", werr)
}
}
// Surface R EXACTLY ONCE — to stdout, with a write-it-down banner. Never logged/persisted.
fmt.Println()