v0.105.0: fork-4 offsite password custody — hand-off + atomicity gate + DR inject + coord
Pairs with agent v0.77.0. StageEscrowSecret pushes the repo password to the agent (POST /escrow/stage-secret) at offsite-enable → EscrowState="pending". Atomicity gate: RunOffboxBackup (scheduler + handler) refuses until EscrowState="escrowed" (operator POST /backup/offbox/confirm-escrow after the escrow ceremony) — no un-recoverable offsite ciphertext can exist. DR: POST /backup/offbox/inject-password pre-places a recovered 64-hex password 0600 (honored by WriteOffboxSecrets' IsNotExist guard; refuses clobber without force). DR recipe gains non-secret offsite_restic coords (DRResticCoord); SFTP key regenerated at DR, not escrowed. New settings.OffboxTarget.EscrowState. Tests + atomicity & inject companion red-proofs green; UI gates pass. 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:
@@ -165,6 +165,12 @@ func BuildReport(
|
||||
// Allowlist-only (the boundary): NO env/secret fields. The hub assembles it with the agent half.
|
||||
r.DRRecipe = BuildDRRecipeAppHalf(cfg.Customer.ID, cfg.Customer.Name, cfg.Customer.Domain,
|
||||
stackMgr.GetStacks(), readComposeFile)
|
||||
// fork-4: attach the non-secret offsite restic repo coordinates so DR knows where to recover from.
|
||||
if backupMgr != nil {
|
||||
if host, user, port, repoPath, ok := backupMgr.OffboxCoord(); ok {
|
||||
r.DRRecipe.OffsiteRestic = &DRResticCoord{Host: host, User: user, Port: port, RepoPath: repoPath}
|
||||
}
|
||||
}
|
||||
|
||||
if debug && logger != nil {
|
||||
logger.Printf("[DEBUG] [report] BuildReport: complete — containers=%d, health=%s, deployed=%d, available=%d, app_telemetry=%d",
|
||||
|
||||
@@ -36,6 +36,19 @@ type DRRecipeAppHalf struct {
|
||||
RecipeVersion int `json:"recipe_version"`
|
||||
Customer DRCustomer `json:"customer"`
|
||||
Apps []AppRecipe `json:"apps"`
|
||||
// OffsiteRestic (fork-4) is the non-secret location of the offsite restic repo, so DR knows WHERE to
|
||||
// recover from. nil when offsite is not configured. Coordinates ONLY — see DRResticCoord.
|
||||
OffsiteRestic *DRResticCoord `json:"offsite_restic,omitempty"`
|
||||
}
|
||||
|
||||
// DRResticCoord is the offsite restic repo's non-secret coordinates. The repo PASSWORD rides the R-escrow
|
||||
// (IdentityBundle.ResticRepoPassword); the SFTP access key is regenerated at DR (a fresh sub-account key) —
|
||||
// so NEITHER appears here. All field names deliberately clear the _NoSecrets regex (no password/key/token).
|
||||
type DRResticCoord struct {
|
||||
Host string `json:"host"`
|
||||
User string `json:"user"`
|
||||
Port int `json:"port"`
|
||||
RepoPath string `json:"repo_path"`
|
||||
}
|
||||
|
||||
// DRCustomer is the customer identity — public identifiers only.
|
||||
|
||||
@@ -99,6 +99,36 @@ func TestBuildAppRecipe_AllowlistIsLoadBearing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Scenario E (fork-4) — the OffsiteRestic DR coord carries coordinates ONLY (no password/key), clears the
|
||||
// secret-name regex, and its fields are emitted. Extends the _NoSecrets boundary to the new field.
|
||||
func TestDRResticCoord_NoSecrets(t *testing.T) {
|
||||
half := &DRRecipeAppHalf{
|
||||
RecipeVersion: DRRecipeVersion,
|
||||
Customer: DRCustomer{ID: "cust", Display: "Cust", Domain: "demo-felhom.eu"},
|
||||
Apps: []AppRecipe{},
|
||||
OffsiteRestic: &DRResticCoord{
|
||||
Host: "u629193-sub1.your-storagebox.de", User: "u629193-sub1", Port: 23, RepoPath: "/home/felhom-demo-repo",
|
||||
},
|
||||
}
|
||||
b, err := json.Marshal(half)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// (1) no credential-shaped KEY name survived (the DRResticCoord field names must clear the regex).
|
||||
assertNoSecretKeys(t, b)
|
||||
out := string(b)
|
||||
// (2) positive: the coordinate fields ARE emitted (not a vacuous pass).
|
||||
for _, want := range []string{`"offsite_restic"`, `"host":"u629193-sub1.your-storagebox.de"`, `"user":"u629193-sub1"`, `"port":23`, `"repo_path":"/home/felhom-demo-repo"`} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("DR coord missing %s in %s", want, out)
|
||||
}
|
||||
}
|
||||
// (3) the password/key field names must never appear.
|
||||
if strings.Contains(out, "repo_password") || strings.Contains(out, "ssh_key") || strings.Contains(out, "password") {
|
||||
t.Fatalf("DR coord must carry NO password/key: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppStorageBindings(t *testing.T) {
|
||||
got := appStorageBindings(rommCompose, "/mnt/felhom-drives/felhom-flash")
|
||||
want := map[string]StorageBinding{
|
||||
|
||||
Reference in New Issue
Block a user