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:
@@ -822,6 +822,48 @@ func SensitiveEnvVars(meta *Metadata) []string {
|
||||
return vars
|
||||
}
|
||||
|
||||
// nonPortableSecrets is the register of secrets that must NEVER travel in an on-drive recovery unit
|
||||
// even though the catalog types them `secret` — i.e. credentials whose reach is NOT bounded by
|
||||
// physical possession of the drive, because they authenticate against a service published to the
|
||||
// internet. Keyed by catalog SLUG (never empty — LoadMetadata falls back to the directory name).
|
||||
//
|
||||
// It is a CODE register, not a catalog flag, deliberately: the D5 ruling is a security boundary, and
|
||||
// a boundary a catalog push can silently move is not a boundary (the R-97a lesson — an invariant that
|
||||
// only configuration enforced). Adding an app whose `type: secret` field gates an internet-reachable
|
||||
// login means adding a row here.
|
||||
//
|
||||
// - vaultwarden/ADMIN_TOKEN gates the /admin panel, served on the app's own public web port.
|
||||
var nonPortableSecrets = map[string]map[string]bool{
|
||||
"vaultwarden": {"ADMIN_TOKEN": true},
|
||||
}
|
||||
|
||||
// PortableSecretEnvVars returns the env-var names of secrets that TRAVEL inside the on-drive recovery
|
||||
// unit (D5), in deterministic metadata order.
|
||||
//
|
||||
// The ruling (operator, 2026-07-30): `type: secret` travels, `type: password` does not, minus
|
||||
// nonPortableSecrets. The line is drawn on REACH, not on whether a secret is nominally resettable:
|
||||
//
|
||||
// - Every `type: secret` field either decrypts data sitting on the SAME drive (the 5 declared
|
||||
// data_keys, plus encryption keys the catalog labels as such but never flagged — see R-127) or
|
||||
// authenticates to a container on an internal compose network with no external listener (the 18
|
||||
// DB/root passwords, and the signing secrets). Possessing it adds nothing to possessing the
|
||||
// drive, which is exactly D2's argument for keeping the DATA plaintext.
|
||||
// - Every `type: password` field is an admin/UI login for a published service, so its blast radius
|
||||
// is NOT bounded by the drive. Those stay in the guest and are regenerated on restore (O4).
|
||||
//
|
||||
// Excluding the `type: password` class is what licenses the plaintext ruling; the two are coupled and
|
||||
// must not be relaxed independently.
|
||||
func PortableSecretEnvVars(meta *Metadata) []string {
|
||||
blocked := nonPortableSecrets[meta.Slug]
|
||||
var vars []string
|
||||
for _, f := range meta.DeployFields {
|
||||
if f.Type == "secret" && !blocked[f.EnvVar] {
|
||||
vars = append(vars, f.EnvVar)
|
||||
}
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
// --- Secret generation ---
|
||||
|
||||
const alphanumChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
Reference in New Issue
Block a user