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
+9 -4
View File
@@ -12,6 +12,7 @@ import (
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/crypto"
"gitea.dooplex.hu/admin/felhom-controller/internal/scheduler"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
@@ -300,9 +301,13 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
data["AutoFields"] = meta.AutoGeneratedFields()
// Auto-generated field values: existing values for deployed apps, pre-generated for new deploys
autoFieldValues := make(map[string]string)
var decryptedEnv map[string]string
if appCfg != nil {
decryptedEnv = crypto.DecryptMap(s.encKey, appCfg.Env)
}
if alreadyDeployed && appCfg != nil {
for _, f := range meta.AutoGeneratedFields() {
if val, ok := appCfg.Env[f.EnvVar]; ok {
if val, ok := decryptedEnv[f.EnvVar]; ok {
autoFieldValues[f.EnvVar] = val
}
}
@@ -314,9 +319,9 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
}
}
data["AutoFieldValues"] = autoFieldValues
// For deployed apps, pass stored field values so subdomain and other user fields show current values
if alreadyDeployed && appCfg != nil {
data["DeployedFieldValues"] = appCfg.Env
// For deployed apps, pass stored field values (decrypted) so fields show current values
if alreadyDeployed && decryptedEnv != nil {
data["DeployedFieldValues"] = decryptedEnv
}
// Storage paths with free space info for deploy dropdown
var deployPaths []DeployStoragePath
+6
View File
@@ -37,6 +37,7 @@ type Server struct {
updater *selfupdate.Updater
logger *log.Logger
version string
encKey []byte // AES-256 key for decrypting app.yaml values
tmpl *template.Template
sessions map[string]*session
@@ -107,6 +108,11 @@ func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *syste
return s
}
// SetEncryptionKey sets the AES-256 key used to decrypt app.yaml values for display.
func (s *Server) SetEncryptionKey(key []byte) {
s.encKey = key
}
func (s *Server) loadTemplates() {
s.tmpl = template.Must(
template.New("").Funcs(s.templateFuncMap()).ParseFS(templateFS, "templates/*.html"),
+2 -1
View File
@@ -596,7 +596,8 @@ func (s *Server) updateStackHDDPath(stackName, newPath string) error {
return fmt.Errorf("app.yaml not found for stack: %s", stackName)
}
appCfg.Env["HDD_PATH"] = newPath
return stacks.SaveAppConfig(stackDir, appCfg)
meta := stacks.LoadMetadata(stackDir)
return stacks.SaveAppConfig(stackDir, appCfg, s.encKey, stacks.SensitiveEnvVars(&meta))
}
// storageInfoForStack returns deploy storage info for a deployed stack.