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
+19 -1
View File
@@ -19,6 +19,7 @@ import (
"gitea.dooplex.hu/admin/felhom-controller/internal/api"
"gitea.dooplex.hu/admin/felhom-controller/internal/assets"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/crypto"
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
"gitea.dooplex.hu/admin/felhom-controller/internal/metrics"
"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
@@ -88,11 +89,20 @@ func main() {
discoveredPaths := discoverHDDPaths(cfg.Paths.StacksDir, logger)
sett.AutoDiscoverStoragePaths(discoveredPaths, cfg.Paths.HDDPath, logger)
// --- Load or create encryption key ---
encKeyPath := filepath.Join(cfg.Paths.DataDir, "encryption.key")
encKey, err := crypto.LoadOrCreateKey(encKeyPath)
if err != nil {
logger.Fatalf("[FATAL] Failed to load encryption key: %v", err)
}
logger.Printf("[INFO] Encryption key loaded from %s", encKeyPath)
// --- Initialize stack manager ---
stackMgr, err := stacks.NewManager(cfg, logger)
if err != nil {
logger.Fatalf("[FATAL] Failed to initialize stack manager: %v", err)
}
stackMgr.SetEncryptionKey(encKey)
// Initial stack scan
if err := stackMgr.ScanStacks(); err != nil {
@@ -104,6 +114,9 @@ func main() {
stackMgr.InjectMissingFields(names)
}
// Migrate existing plaintext passwords to encrypted
stackMgr.MigrateEncryption()
// --- Initialize catalog syncer ---
syncer := catalogsync.New(cfg, logger, stackMgr.ScanStacks, func(updated []string) {
stackMgr.InjectMissingFields(updated)
@@ -590,6 +603,7 @@ func main() {
}
// --- Initialize web server ---
webServer := web.NewServer(cfg, stackMgr, cpuCollector, backupMgr, crossDriveRunner, sched, sett, alertMgr, notifier, updater, logger, Version)
webServer.SetEncryptionKey(encKey)
webServer.SetStorageWatchdog(storageWatchdog)
if assetsSyncer != nil {
webServer.SetAssetsSyncer(assetsSyncer)
@@ -942,7 +956,7 @@ func (a *driveMigrateStackAdapter) UpdateStackHDDPath(name, newPath string) erro
return fmt.Errorf("app.yaml not found for stack: %s", name)
}
appCfg.Env["HDD_PATH"] = newPath
return stacks.SaveAppConfig(stackDir, appCfg)
return stacks.SaveAppConfig(stackDir, appCfg, nil, nil)
}
func (a *driveMigrateStackAdapter) StackExists(name string) bool {
@@ -954,11 +968,13 @@ func (a *driveMigrateStackAdapter) StackExists(name string) bool {
func pushInfraBackup(cfg *config.Config, sett *settings.Settings,
stackProv *stackAdapter, pusher *report.Pusher, logger *log.Logger) {
encKeyPath := filepath.Join(cfg.Paths.DataDir, "encryption.key")
ib, err := report.BuildInfraBackup(
cfg.Customer.ID, cfg.Customer.Domain, Version,
"/opt/docker/felhom-controller/controller.yaml",
filepath.Join(cfg.Paths.DataDir, "settings.json"),
cfg.Backup.ResticPasswordFile,
encKeyPath,
cfg.Paths.SystemDataPath,
sett, stackProv, logger,
)
@@ -1053,11 +1069,13 @@ func runSetupMode(cfg *config.Config, logger *log.Logger) {
func writeLocalInfraBackup(cfg *config.Config, sett *settings.Settings,
stackProv *stackAdapter, logger *log.Logger) {
encKeyPath := filepath.Join(cfg.Paths.DataDir, "encryption.key")
ib, err := report.BuildInfraBackup(
cfg.Customer.ID, cfg.Customer.Domain, Version,
"/opt/docker/felhom-controller/controller.yaml",
filepath.Join(cfg.Paths.DataDir, "settings.json"),
cfg.Backup.ResticPasswordFile,
encKeyPath,
cfg.Paths.SystemDataPath,
sett, stackProv, logger,
)