v0.324.0: the database password comes back with the data (R-1106, R-127b, R-926); restic 0.19.1 (R-1107); R-1108, R-1065, R-1017; dashboard hardening (R-983, R-976)
gates / gates (push) Successful in 44s
gates / gates (push) Successful in 44s
This commit is contained in:
@@ -828,12 +828,31 @@ func (m *Manager) ReconstituteFromOffsite(ctx context.Context, stack string, ack
|
||||
}
|
||||
}
|
||||
|
||||
// --- THE DATABASE PASSWORD COMES BACK WITH THE DATA (R-1106, `07` §7.4, decision 1A) ---------
|
||||
// When the app keeps its own definition (same version), its app.yaml still holds the CURRENT install's database
|
||||
// password — after a remove + reinstall, a freshly generated one that cannot open the data directory this restore
|
||||
// is about to put back (measured 2026-10-16: Rallly crash-looped on auth, Grimmory was held). So when the snapshot
|
||||
// replays a database service's data VOLUME, the snapshot's password is written into the app's settings before
|
||||
// anything starts. A snapshot that replays only a logical dump leaves the current password alone — the dump goes
|
||||
// into the current data directory, which the current password opens. Decided before the first mutation; a
|
||||
// refusal touches nothing. (The other branch already gets this through unitRestoreEnv.)
|
||||
var credOverlay map[string]string
|
||||
if !defineFromSnapshot {
|
||||
ov, oErr := m.offsiteCredentialOverlay(stack, scratchUnit, scratchCompose, man)
|
||||
if oErr != nil {
|
||||
return res, oErr
|
||||
}
|
||||
credOverlay = ov
|
||||
}
|
||||
definitionMoved := defineFromSnapshot || len(credOverlay) > 0
|
||||
|
||||
// --- THE LIVE DEFINITION, KEPT FOR A FAILED REPLAY (R-893, `09` §3 decision 192) -------------
|
||||
// When the snapshot's definition is about to be written, keep the LIVE one first, so a failed
|
||||
// replay below can write it back before the app is held. Taken before the first mutation; a
|
||||
// failure here refuses with nothing touched — without it the failure branch could not be undone.
|
||||
// R-1106: the same when only the database password is about to move.
|
||||
var liveDef *liveDefinition
|
||||
if defineFromSnapshot {
|
||||
if definitionMoved {
|
||||
ld, ldErr := m.captureLiveDefinition(stack)
|
||||
if ldErr != nil {
|
||||
m.logger.Printf("[ERROR] [offbox] Restore REFUSED for %s: could not keep the live definition: %v — nothing was touched", stack, ldErr)
|
||||
@@ -919,6 +938,14 @@ func (m *Manager) ReconstituteFromOffsite(ctx context.Context, stack string, ack
|
||||
}
|
||||
return res, fmt.Errorf("restoring %s: writing the snapshot's definition failed: %w", stack, err)
|
||||
}
|
||||
} else if len(credOverlay) > 0 {
|
||||
if err := m.stackProvider.(secretValueSetter).SetStackSecretValues(stack, credOverlay); err != nil {
|
||||
if sErr := restartStack(); sErr != nil {
|
||||
m.logger.Printf("[WARN] [offbox] %s: restart after a failed password write also failed: %v", stack, sErr)
|
||||
}
|
||||
return res, fmt.Errorf("restoring %s: writing the backup's database password failed: %w", stack, err)
|
||||
}
|
||||
m.logger.Printf("[INFO] [offbox] %s: the backup's database password(s) %v written into the app's settings before the data (R-1106)", stack, credNames(credOverlay))
|
||||
}
|
||||
copier := m.fullPlaceCopier()
|
||||
for _, pl := range placements {
|
||||
@@ -943,7 +970,7 @@ func (m *Manager) ReconstituteFromOffsite(ctx context.Context, stack string, ack
|
||||
if cErr != nil {
|
||||
// R-893 option C (security review 2026-10-08): once the snapshot's (possibly older) definition
|
||||
// is written, a start would run that version on the live data — HOLD the app instead.
|
||||
if defineFromSnapshot {
|
||||
if definitionMoved {
|
||||
m.holdAppAfterMixedRestore(stack, cErr, liveDef)
|
||||
return res, util.MsgError("err.backup.restore_failed_held_mixed", stack)
|
||||
}
|
||||
@@ -977,7 +1004,7 @@ func (m *Manager) ReconstituteFromOffsite(ctx context.Context, stack string, ack
|
||||
// already be the snapshot's — HOLD the app rather than start it on a mix.
|
||||
// R-893 option C (security review 2026-10-08): a volume already replaced, or the snapshot's
|
||||
// definition written → HOLD (the volume detail reaches the operator through the hold notice).
|
||||
if defineFromSnapshot || nVols > 0 {
|
||||
if definitionMoved || nVols > 0 {
|
||||
m.holdAppAfterMixedRestore(stack, vErr, liveDef)
|
||||
return res, util.MsgError("err.backup.restore_failed_held_mixed", stack)
|
||||
}
|
||||
@@ -997,7 +1024,7 @@ func (m *Manager) ReconstituteFromOffsite(ctx context.Context, stack string, ack
|
||||
if hasDB {
|
||||
if err := m.stackProvider.StartStackServices(stack, dbServices); err != nil {
|
||||
// R-893 option C (security review 2026-10-08): volumes or the definition already moved → HOLD.
|
||||
if defineFromSnapshot || res.VolumesReplayed > 0 {
|
||||
if definitionMoved || res.VolumesReplayed > 0 {
|
||||
m.holdAppAfterMixedRestore(stack, err, liveDef)
|
||||
return res, util.MsgError("err.backup.restore_failed_held_mixed", stack)
|
||||
}
|
||||
@@ -1044,7 +1071,7 @@ func (m *Manager) ReconstituteFromOffsite(ctx context.Context, stack string, ack
|
||||
// Pinned by r893_mixed_restore_test.go; the plain case keeps TestR379_ScenarioA.
|
||||
// Placed FILES alone do not hold (option C as ruled): that mix is what „put back exactly as it
|
||||
// was" (option A, the next slice) removes.
|
||||
if defineFromSnapshot || res.VolumesReplayed > 0 {
|
||||
if definitionMoved || res.VolumesReplayed > 0 {
|
||||
m.holdAppAfterMixedRestore(stack, iErr, liveDef)
|
||||
return res, util.MsgError("err.backup.db_restore_failed_held_mixed", stack)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user