R-757: a secret only after_install reads is not invented for an installed app

InjectMissingFields skips a new generated secret that is in after_install's
env list and named by no compose definition: after_install does not run
again, so the app never received the value and the reveal would answer
a login the app does not have (calibre-web ADMIN_USER, 2026-10-01).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 22:20:58 +02:00
parent 4347983a72
commit 1991c742a3
2 changed files with 74 additions and 0 deletions
+26
View File
@@ -1337,6 +1337,14 @@ func (m *Manager) InjectMissingFields(stackNames []string) {
switch field.Type {
case "secret":
// R-757: a value the app must have been GIVEN — read only by the one-time after_install
// command, never by the compose file — cannot be invented for an app already installed:
// after_install does not run again, so the app never received it, and the reveal would
// then answer a login (calibre-web's ADMIN_USER, measured 2026-10-01) the app does not have.
if onlyGivenAtInstall(&meta, stackDir, field.EnvVar) {
m.logger.Printf("[INFO] [stacks] Stack %s: new field %s is read only by after_install, which ran (or not) at install — not invented for an installed app (R-757)", name, field.EnvVar)
continue
}
if field.Generate == "" {
m.logger.Printf("[WARN] [stacks] Stack %s: new secret field %s has no generator — skipping", name, field.EnvVar)
continue
@@ -1391,6 +1399,24 @@ func (m *Manager) InjectMissingFields(stackNames []string) {
m.logger.Printf("[INFO] [stacks] InjectMissingFields: processed %d stacks", count)
}
// onlyGivenAtInstall reports whether envVar reaches the app ONLY through the one-time after_install
// command: it is in after_install's env list and no compose definition the app runs from (the catalog
// mirror, or the pinned applied copy) names it. Unreadable compose files count as NOT naming it, which is
// the side that skips the injection — inventing nothing is the safe error. Pinned by
// TestR757_AfterInstallOnlyFieldIsNotInvented.
func onlyGivenAtInstall(meta *Metadata, stackDir, envVar string) bool {
if meta == nil || meta.AfterInstall == nil || !containsStr(meta.AfterInstall.Env, envVar) {
return false
}
ref := regexp.MustCompile(`\$\{?` + regexp.QuoteMeta(envVar) + `\b`)
for _, p := range []string{filepath.Join(stackDir, "docker-compose.yml"), AppliedComposePath(stackDir)} {
if b, err := os.ReadFile(p); err == nil && ref.Match(b) {
return false
}
}
return true
}
func containsStr(slice []string, s string) bool {
for _, v := range slice {
if v == s {