feat: encrypt sensitive values in app.yaml with AES-256-GCM

Passwords and secrets from deploy fields (type: password/secret) are now
encrypted at rest in app.yaml using a per-node 32-byte key. Values stored
as ENC:base64(nonce+ciphertext), decrypted transparently for docker-compose
and web UI. Key included in infra backup bundle for disaster recovery.
Existing plaintext values migrated automatically on startup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 19:12:24 +01:00
parent 703dee15ab
commit 44f7fd2f19
11 changed files with 297 additions and 15 deletions
@@ -26,6 +26,7 @@ type InfraBackup struct {
ResticPassword string `json:"restic_password,omitempty"`
CrossDrivePassword string `json:"cross_drive_password,omitempty"`
EncryptionKeyB64 string `json:"encryption_key_b64,omitempty"`
}
// InfraStack identifies a deployed app for disaster recovery.
@@ -42,6 +43,7 @@ func BuildInfraBackup(
controllerYAMLPath string,
settingsPath string,
resticPasswordFile string,
encryptionKeyFile string,
systemDataPath string,
sett *settings.Settings,
stackProvider backup.StackDataProvider,
@@ -75,6 +77,15 @@ func BuildInfraBackup(
logger.Printf("[WARN] Infra backup: could not read restic password file: %v", err)
}
// Read encryption key for app.yaml secrets (important but non-fatal)
if encryptionKeyFile != "" {
if data, err := os.ReadFile(encryptionKeyFile); err == nil {
ib.EncryptionKeyB64 = base64.StdEncoding.EncodeToString(data)
} else if !os.IsNotExist(err) {
logger.Printf("[WARN] Infra backup: could not read encryption key file: %v", err)
}
}
// Collect disk layout from fstab + blkid
ib.DiskLayout = collectDiskLayout(systemDataPath)