D5: an app restore works from the drive alone (v0.188.0)
The recovery unit on the customer's drive now carries the PORTABLE secret class, so Tier-1/Tier-2 restore no longer depends on the whole-guest tier. A customer needs the drive and nothing else. Part 0's rulings overturned the brief's recommendation, on evidence: - the data_key flag is untrustworthy (4+ encryption keys the catalog itself labels as such are unflagged) -> R-127 - a DB password is not resettable in practice: POSTGRES_PASSWORD is ignored once PGDATA is non-empty, so a regenerated value leaves the app unable to authenticate against its own restored rows while the dump replay still reports success (proven on a throwaway postgres:16-alpine) Ruling (operator): type:secret travels, type:password never does, minus the nonPortableSecrets code register. Plaintext -- withholding the internet- reachable class is what licenses that, and the two are coupled. Precedence: the UNIT WINS over the guest -- the unit's secrets were captured in the same run as the dumps beside them, so they match the data being restored. The fail-closed data-key gate is unchanged. Secret values are never logged; the manifest records NAMES only.
This commit is contained in:
@@ -1440,12 +1440,14 @@ func (a *stackAdapter) GetStackHDDPath(name string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetStackRecoveryInfo gathers the SECRET-FREE inputs for an app's recovery unit (Phase 2): the
|
||||
// stack dir, pinned image tags, the non-secret env, and the NAMES of secret/data-key env vars.
|
||||
// It deliberately does NOT decrypt or return any secret value — secret/password fields are stored
|
||||
// encrypted in app.yaml, so excluding them (plus a defensive crypto.IsEncrypted guard) yields a
|
||||
// plaintext, secret-free env. The actual secret values are recovered at restore time from the
|
||||
// guest's own app.yaml (live, or via the PBS whole-guest snapshot), never from the unit.
|
||||
// GetStackRecoveryInfo gathers the inputs for an app's recovery unit: the stack dir, pinned image
|
||||
// tags, the non-secret env, the NAMES of every secret/data-key env var, and — since D5 — the decrypted
|
||||
// VALUES of the PORTABLE secret class.
|
||||
//
|
||||
// The non-secret env still excludes every named secret (plus a defensive crypto.IsEncrypted guard), so
|
||||
// the two sets are disjoint by construction and a secret can only reach the unit by being in the
|
||||
// portable class. The EXCLUDED class (`type: password`, and the nonPortableSecrets register) is
|
||||
// name-only here and is recovered from the guest — or regenerated (O4) — exactly as before.
|
||||
func (a *stackAdapter) GetStackRecoveryInfo(name string) (backup.RecoveryInfo, bool) {
|
||||
s, ok := a.mgr.GetStack(name)
|
||||
if !ok {
|
||||
@@ -1482,13 +1484,31 @@ func (a *stackAdapter) GetStackRecoveryInfo(name string) (backup.RecoveryInfo, b
|
||||
}
|
||||
}
|
||||
|
||||
// D5: decrypt the portable class so it can travel in the unit. Reuses the SAME decrypt path as the
|
||||
// restore side (LoadAppConfigDecrypted), so there is one way to turn app.yaml into plaintext, not
|
||||
// two. A name whose value is absent/empty is simply omitted — the restore's fail-closed data-key
|
||||
// gate is what decides whether that is survivable.
|
||||
portableNames := stacks.PortableSecretEnvVars(&meta)
|
||||
portable := make(map[string]string, len(portableNames))
|
||||
if len(portableNames) > 0 {
|
||||
if dec := stacks.LoadAppConfigDecrypted(stackDir, a.encKey); dec != nil {
|
||||
for _, n := range portableNames {
|
||||
if v, ok := dec.Env[n]; ok && v != "" {
|
||||
portable[n] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return backup.RecoveryInfo{
|
||||
StackDir: stackDir,
|
||||
DisplayName: s.Meta.DisplayName,
|
||||
ImagePins: backup.ParseComposeImages(s.ComposePath),
|
||||
NonSecretEnv: nonSecret,
|
||||
SecretEnvVars: secretNames,
|
||||
DataKeyEnvVars: dataKeys,
|
||||
StackDir: stackDir,
|
||||
DisplayName: s.Meta.DisplayName,
|
||||
ImagePins: backup.ParseComposeImages(s.ComposePath),
|
||||
NonSecretEnv: nonSecret,
|
||||
SecretEnvVars: secretNames,
|
||||
DataKeyEnvVars: dataKeys,
|
||||
PortableSecretEnvVars: portableNames,
|
||||
PortableSecrets: portable,
|
||||
}, true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user