From 6d026f9cf1ea28c0bef580ae6e4c90c0b73e5cdf Mon Sep 17 00:00:00 2001 From: Renuka Fernando Date: Thu, 27 Aug 2026 12:51:08 +0530 Subject: [PATCH 1/2] feat(helm): fail on contradictory storage dsn + passwordSecretRef A full storage dsn is used verbatim by the controller (buildPostgresDSN / buildSQLServerDSN); the separately-injected passwordSecretRef password is never merged into it. Setting both silently dropped the secret, leaving the controller to connect with no password and crashloop on an opaque DB auth error. - Add a render-time guard to gateway-config.yaml (postgres and sqlserver blocks) that fails when a dsn is set alongside its passwordSecretRef unless the dsn references the injected password env token. - Rewrite the misleading values.yaml comments that implied a password-less dsn plus a separate secret works. Refs #3318 Signed-off-by: Renuka Fernando --- .../templates/gateway/gateway-config.yaml | 22 +++++++++ .../helm/gateway-helm-chart/values.yaml | 48 ++++++++++++++----- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/kubernetes/helm/gateway-helm-chart/templates/gateway/gateway-config.yaml b/kubernetes/helm/gateway-helm-chart/templates/gateway/gateway-config.yaml index 26bb9fcb58..42b9443960 100644 --- a/kubernetes/helm/gateway-helm-chart/templates/gateway/gateway-config.yaml +++ b/kubernetes/helm/gateway-helm-chart/templates/gateway/gateway-config.yaml @@ -1,4 +1,5 @@ {{- $gc := .Values.gateway.config.controller -}} +{{- $controller := .Values.gateway.controller -}} {{- $router := .Values.gateway.config.router -}} {{- $pe := .Values.gateway.config.policy_engine -}} {{- $pg := $gc.storage.postgres -}} @@ -75,6 +76,17 @@ data: {{- if eq $gc.storage.type "postgres" }} [controller.storage.postgres] {{- if $pg.dsn }} + {{- /* + A full DSN is used verbatim by the controller (buildPostgresDSN returns it + as-is when set) — the password from gateway.controller.postgres.passwordSecretRef + is NOT merged into it. Setting both, unless the DSN itself references the + injected password env token, means the secret is silently ignored and the + controller connects with no password. Fail loudly instead. The secure way to + combine a DSN with the secret is to embed the token in the DSN. + */ -}} + {{- if and $controller.postgres.passwordSecretRef.name (not (contains "APIP_GW_CONTROLLER_STORAGE_POSTGRES_PASSWORD" $pg.dsn)) }} + {{- fail "gateway.config.controller.storage.postgres.dsn is set together with gateway.controller.postgres.passwordSecretRef, but the DSN does not reference the injected password. A full DSN is used verbatim; the separately-injected password is ignored. Either embed the token in the DSN, e.g. dsn: 'postgres://user:{{ env \"APIP_GW_CONTROLLER_STORAGE_POSTGRES_PASSWORD\" }}@host:5432/db?sslmode=require', or unset gateway.controller.postgres.passwordSecretRef and put the password directly in the DSN." }} + {{- end }} dsn = {{ $pg.dsn | quote }} {{- else }} host = {{ required "gateway.config.controller.storage.postgres.host is required when storage.type is \"postgres\" and dsn is unset" $pg.host | quote }} @@ -96,6 +108,16 @@ data: [controller.storage.database] driver = {{ default "sqlserver" $db.driver | quote }} {{- if $db.dsn }} + {{- /* + A full DSN is used verbatim by the controller (buildSQLServerDSN returns it + as-is when set) — the password from gateway.controller.sqlserver.passwordSecretRef + is NOT merged into it. Setting both, unless the DSN itself references the + injected password env token, means the secret is silently ignored. Fail loudly + instead; embed the token in the DSN to combine a DSN with the secret. + */ -}} + {{- if and $controller.sqlserver.passwordSecretRef.name (not (contains "APIP_GW_CONTROLLER_STORAGE_DATABASE_PASSWORD" $db.dsn)) }} + {{- fail "gateway.config.controller.storage.database.dsn is set together with gateway.controller.sqlserver.passwordSecretRef, but the DSN does not reference the injected password. A full DSN is used verbatim; the separately-injected password is ignored. Either embed the token in the DSN, e.g. dsn: 'sqlserver://user:{{ env \"APIP_GW_CONTROLLER_STORAGE_DATABASE_PASSWORD\" }}@host:1433?database=gateway', or unset gateway.controller.sqlserver.passwordSecretRef and put the password directly in the DSN." }} + {{- end }} dsn = {{ $db.dsn | quote }} {{- else }} host = {{ required "gateway.config.controller.storage.database.host is required when storage.type is \"sqlserver\" and dsn is unset" $db.host | quote }} diff --git a/kubernetes/helm/gateway-helm-chart/values.yaml b/kubernetes/helm/gateway-helm-chart/values.yaml index 7b028de112..f3708ae0f7 100644 --- a/kubernetes/helm/gateway-helm-chart/values.yaml +++ b/kubernetes/helm/gateway-helm-chart/values.yaml @@ -133,11 +133,19 @@ gateway: sqlite: path: ./data/gateway.db - # PostgreSQL configuration (used when type=postgres) - # Password is injected separately via gateway.controller.postgres.passwordSecretRef + # PostgreSQL configuration (used when type=postgres). Configure EITHER the + # individual fields (host/port/database/user) with the password injected from a + # Secret via gateway.controller.postgres.passwordSecretRef, OR a full dsn below — + # the two are mutually exclusive, not complementary. postgres: - # Full DSN takes precedence over individual fields when set. - # Example: "postgres://user:password@host:5432/dbname?sslmode=require" + # Full DSN. When set, the individual fields below are ignored and the DSN is + # used VERBATIM — the passwordSecretRef password is NOT merged in, so the DSN + # must carry its own credentials. A literal password here is written into the + # ConfigMap in plaintext; to keep it in a Secret while using a DSN, reference + # the injected token in the DSN and keep passwordSecretRef set, e.g.: + # dsn: 'postgres://user:{{ env "APIP_GW_CONTROLLER_STORAGE_POSTGRES_PASSWORD" }}@host:5432/dbname?sslmode=require' + # (The chart fails to render if a DSN is set alongside passwordSecretRef but + # does not reference the token.) dsn: "" host: "" @@ -156,13 +164,21 @@ gateway: conn_max_idle_time: 5m application_name: gateway-controller - # Global database configuration (used for type=sqlserver). - # Password is injected separately via gateway.controller.sqlserver.passwordSecretRef. - # This shape maps to [controller.storage.database] in config.toml. + # Global database configuration (used for type=sqlserver). Configure EITHER the + # individual fields (host/port/database/user) with the password injected from a + # Secret via gateway.controller.sqlserver.passwordSecretRef, OR a full dsn below — + # the two are mutually exclusive. This shape maps to [controller.storage.database] + # in config.toml. database: driver: sqlserver - # Full DSN takes precedence over individual fields when set. - # Example: "sqlserver://gateway@host:1433?database=gateway" + # Full DSN. When set, the individual fields below are ignored and the DSN is + # used VERBATIM — the passwordSecretRef password is NOT merged in, so the DSN + # must carry its own credentials. A literal password here is written into the + # ConfigMap in plaintext; to keep it in a Secret while using a DSN, reference + # the injected token in the DSN and keep passwordSecretRef set, e.g.: + # dsn: 'sqlserver://user:{{ env "APIP_GW_CONTROLLER_STORAGE_DATABASE_PASSWORD" }}@host:1433?database=gateway' + # (The chart fails to render if a DSN is set alongside passwordSecretRef but + # does not reference the token.) dsn: "" host: "" @@ -726,14 +742,20 @@ gateway: secretName: "" # Mount path inside the container — must match controller.encryption.providers[].keys[].file paths mountPath: /app/data/aesgcm-keys - # PostgreSQL password secret reference (used when gateway.config.controller.storage.type=postgres). - # The password is injected from this Secret rather than stored in the ConfigMap (see README). + # PostgreSQL password secret reference (used when gateway.config.controller.storage.type=postgres + # and gateway.config.controller.storage.postgres.dsn is NOT set). The password is injected from + # this Secret rather than stored in the ConfigMap (see README). It has NO effect when a dsn is set + # unless the dsn references the injected token {{ env "APIP_GW_CONTROLLER_STORAGE_POSTGRES_PASSWORD" }}; + # the chart fails to render if a dsn is set alongside this without referencing the token. postgres: passwordSecretRef: name: "" key: password - # SQL Server password secret reference (used when gateway.config.controller.storage.type=sqlserver). - # The password is injected from this Secret rather than stored in the ConfigMap (see README). + # SQL Server password secret reference (used when gateway.config.controller.storage.type=sqlserver + # and gateway.config.controller.storage.database.dsn is NOT set). The password is injected from + # this Secret rather than stored in the ConfigMap (see README). It has NO effect when a dsn is set + # unless the dsn references the injected token {{ env "APIP_GW_CONTROLLER_STORAGE_DATABASE_PASSWORD" }}; + # the chart fails to render if a dsn is set alongside this without referencing the token. sqlserver: passwordSecretRef: name: "" From 1a0f98c72c8ba0ec130b4eb85be4dbe654068ea8 Mon Sep 17 00:00:00 2001 From: Renuka Fernando Date: Thu, 27 Aug 2026 13:04:55 +0530 Subject: [PATCH 2/2] fix(helm): match full env token in dsn password guard Address CodeRabbit review on #3319: the guard used a substring match on the bare env-var name, so a DSN mentioning the name outside a real interpolation token (e.g. in the dbname) wrongly passed while the passwordSecretRef stayed ignored. Match the full {{ env "VAR" }} token via regexMatch instead, tolerating the {{- trim marker, spacing, and the "" default arg so legitimate token forms still render. Refs #3318 Signed-off-by: Renuka Fernando --- .../templates/gateway/gateway-config.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/kubernetes/helm/gateway-helm-chart/templates/gateway/gateway-config.yaml b/kubernetes/helm/gateway-helm-chart/templates/gateway/gateway-config.yaml index 42b9443960..6ff259d35f 100644 --- a/kubernetes/helm/gateway-helm-chart/templates/gateway/gateway-config.yaml +++ b/kubernetes/helm/gateway-helm-chart/templates/gateway/gateway-config.yaml @@ -82,9 +82,12 @@ data: is NOT merged into it. Setting both, unless the DSN itself references the injected password env token, means the secret is silently ignored and the controller connects with no password. Fail loudly instead. The secure way to - combine a DSN with the secret is to embed the token in the DSN. + combine a DSN with the secret is to embed the token in the DSN. Match the full + `{{ env "VAR" }}` interpolation token (tolerating the {{- trim marker, spacing, + and the "" default arg), not just the bare variable name, so a DSN that merely + mentions the name elsewhere (e.g. in the dbname) still fails the guard. */ -}} - {{- if and $controller.postgres.passwordSecretRef.name (not (contains "APIP_GW_CONTROLLER_STORAGE_POSTGRES_PASSWORD" $pg.dsn)) }} + {{- if and $controller.postgres.passwordSecretRef.name (not (regexMatch `\{\{-? *env +"APIP_GW_CONTROLLER_STORAGE_POSTGRES_PASSWORD"` $pg.dsn)) }} {{- fail "gateway.config.controller.storage.postgres.dsn is set together with gateway.controller.postgres.passwordSecretRef, but the DSN does not reference the injected password. A full DSN is used verbatim; the separately-injected password is ignored. Either embed the token in the DSN, e.g. dsn: 'postgres://user:{{ env \"APIP_GW_CONTROLLER_STORAGE_POSTGRES_PASSWORD\" }}@host:5432/db?sslmode=require', or unset gateway.controller.postgres.passwordSecretRef and put the password directly in the DSN." }} {{- end }} dsn = {{ $pg.dsn | quote }} @@ -113,9 +116,12 @@ data: as-is when set) — the password from gateway.controller.sqlserver.passwordSecretRef is NOT merged into it. Setting both, unless the DSN itself references the injected password env token, means the secret is silently ignored. Fail loudly - instead; embed the token in the DSN to combine a DSN with the secret. + instead; embed the token in the DSN to combine a DSN with the secret. Match the + full `{{ env "VAR" }}` interpolation token (tolerating the {{- trim marker, + spacing, and the "" default arg), not just the bare variable name, so a DSN that + merely mentions the name elsewhere (e.g. in the dbname) still fails the guard. */ -}} - {{- if and $controller.sqlserver.passwordSecretRef.name (not (contains "APIP_GW_CONTROLLER_STORAGE_DATABASE_PASSWORD" $db.dsn)) }} + {{- if and $controller.sqlserver.passwordSecretRef.name (not (regexMatch `\{\{-? *env +"APIP_GW_CONTROLLER_STORAGE_DATABASE_PASSWORD"` $db.dsn)) }} {{- fail "gateway.config.controller.storage.database.dsn is set together with gateway.controller.sqlserver.passwordSecretRef, but the DSN does not reference the injected password. A full DSN is used verbatim; the separately-injected password is ignored. Either embed the token in the DSN, e.g. dsn: 'sqlserver://user:{{ env \"APIP_GW_CONTROLLER_STORAGE_DATABASE_PASSWORD\" }}@host:1433?database=gateway', or unset gateway.controller.sqlserver.passwordSecretRef and put the password directly in the DSN." }} {{- end }} dsn = {{ $db.dsn | quote }}