fix(config): DEPL-004 — expand $(POSTGRES_PASSWORD) placeholder in CERTCTL_DATABASE_URL

Sprint 3 unified-master-audit closure. The Helm chart's _helpers.tpl
(line 133) renders the bundled-Postgres URL with a literal
'$(POSTGRES_PASSWORD)' placeholder:

    postgres://certctl:$(POSTGRES_PASSWORD)@db:5432/certctl?sslmode=disable

Kubernetes' '$(VAR)' env-substitution syntax ONLY expands when the
value is a string literal in the Pod spec. Values sourced from
'valueFrom.secretKeyRef' (which is how the chart wires
CERTCTL_DATABASE_URL) are NOT expanded — the literal makes it all
the way to the server, which tries to dial Postgres with
'$(POSTGRES_PASSWORD)' as the password, fails with auth error, and
leaks the placeholder into application error logs.

Fix: in-process expansion at internal/config/config.expandDatabaseURL.
strings.ReplaceAll of the literal '$(POSTGRES_PASSWORD)' token with
os.Getenv('POSTGRES_PASSWORD') when both the token is present AND
the env var is set. Conservative — no os.ExpandEnv (which would
expand any $VAR), no Docker entrypoint shim, no Helm-template-time
password injection that would inline the secret into a second
Kubernetes resource. External-Postgres deploys whose URL embeds
the real password pass through untouched because the placeholder
doesn't match.

Regression coverage in internal/config/config_test.go pins:
  - happy-path placeholder substitution
  - non-placeholder URL passes through unchanged
  - placeholder + empty POSTGRES_PASSWORD leaves the URL alone
  - multi-occurrence safety via ReplaceAll

Closes DEPL-004.
This commit is contained in:
shankar0123
2026-05-16 04:30:53 +00:00
parent 6a640ac3e7
commit b721596213
2 changed files with 99 additions and 1 deletions
+51
View File
@@ -1918,3 +1918,54 @@ func TestValidate_Bundle2_CORSConcreteAllowlist_Accepted(t *testing.T) {
t.Errorf("Validate() returned %v; want nil for concrete CORS allowlist", err)
}
}
// =============================================================================
// DEPL-004 closure (Sprint 3, 2026-05-16). The Helm chart renders the
// bundled-Postgres URL with a literal "$(POSTGRES_PASSWORD)"
// placeholder. Kubernetes does NOT expand `$(VAR)` syntax when the env
// is sourced from a Secret (valueFrom.secretKeyRef), so the server
// receives the placeholder verbatim. expandDatabaseURL substitutes the
// token with os.Getenv("POSTGRES_PASSWORD") at Load() time.
// =============================================================================
func TestExpandDatabaseURL_SubstitutesPlaceholder(t *testing.T) {
t.Setenv("POSTGRES_PASSWORD", "s3cret!")
in := "postgres://certctl:$(POSTGRES_PASSWORD)@db:5432/certctl?sslmode=disable"
got := expandDatabaseURL(in)
want := "postgres://certctl:s3cret!@db:5432/certctl?sslmode=disable"
if got != want {
t.Errorf("expandDatabaseURL = %q; want %q", got, want)
}
}
func TestExpandDatabaseURL_NoPlaceholderPassesThrough(t *testing.T) {
// External-Postgres deploys bake the password into the URL string
// — the helper must not touch URLs that don't carry the placeholder.
t.Setenv("POSTGRES_PASSWORD", "ignored")
in := "postgres://user:realpw@external:5432/db?sslmode=require"
if got := expandDatabaseURL(in); got != in {
t.Errorf("expandDatabaseURL on non-placeholder URL = %q; want %q (no-op)", got, in)
}
}
func TestExpandDatabaseURL_PlaceholderButNoEnvLeftAlone(t *testing.T) {
// When POSTGRES_PASSWORD is unset, leave the URL alone so the
// downstream connection failure is the same as before (misconfig
// is the operator's, not our regression).
t.Setenv("POSTGRES_PASSWORD", "")
in := "postgres://certctl:$(POSTGRES_PASSWORD)@db:5432/certctl?sslmode=disable"
if got := expandDatabaseURL(in); got != in {
t.Errorf("expandDatabaseURL with no POSTGRES_PASSWORD = %q; want unchanged %q", got, in)
}
}
func TestExpandDatabaseURL_MultipleOccurrences(t *testing.T) {
// Defensive: belt-and-suspenders. The chart only emits one
// placeholder today but ReplaceAll guards against future drift.
t.Setenv("POSTGRES_PASSWORD", "X")
in := "$(POSTGRES_PASSWORD)/$(POSTGRES_PASSWORD)"
want := "X/X"
if got := expandDatabaseURL(in); got != want {
t.Errorf("expandDatabaseURL = %q; want %q", got, want)
}
}