Take the database password from PGPASSWORD, not the DSN
CI / test (pull_request) Successful in 4s
CI / chart (pull_request) Successful in 1s
CI / security (pull_request) Successful in 11s

The chart asked for a whole DSN in a Secret. Nothing writes one: the
Zalando postgres operator generates a Secret with `username` and
`password` keys and no connection string, so wiring the wrapper chart up
would have meant hand-maintaining a second copy of a password the
operator owns and rotates on a from-scratch rebuild -- which is charts#176
again, the issue miniflux closed by doing the opposite.

So the DSN becomes a plain value with no password in it, and the password
arrives as PGPASSWORD from a Secret. pgx fills in from libpq's PG*
environment variables whatever the DSN omits, exactly as miniflux's
lib/pq does. Verified rather than assumed, against a real server: a
password-less DSN connects with PGPASSWORD set, and fails with
`password authentication failed` when it is wrong, so the variable is
doing the work rather than being quietly ignored.

It also keeps the credential out of the rendered manifest and out of
`kubectl describe pod`, which a DSN-with-password does not.
This commit is contained in:
Niklas Ye
2026-09-20 11:00:05 +02:00
parent 21f0eec807
commit cc31c993dd
4 changed files with 47 additions and 23 deletions
@@ -32,14 +32,18 @@ spec:
env:
- name: TERDUT_ADDR
value: ":{{ .Values.service.port }}"
# The connection string, from a Secret: it carries the password.
# The wrapper chart points this at the Secret the Postgres operator
# writes for this database's role.
- name: TERDUT_DB_DSN
value: {{ required "database.dsn is required" .Values.database.dsn | quote }}
{{- if .Values.database.passwordSecret.name }}
# The password reaches pgx through libpq's environment variable
# rather than through the DSN, so it stays out of the rendered
# manifest. pgx fills in from PG* whatever the DSN leaves out.
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: {{ required "database.dsnSecret.name is required" .Values.database.dsnSecret.name }}
key: {{ .Values.database.dsnSecret.key }}
name: {{ .Values.database.passwordSecret.name }}
key: {{ .Values.database.passwordSecret.key }}
{{- end }}
- name: TERDUT_STALE_AFTER
value: "{{ .Values.sweeper.staleAfter }}"
- name: TERDUT_ARCHIVE_AFTER
+20 -8
View File
@@ -11,16 +11,28 @@ image:
tag: "latest"
pullPolicy: IfNotPresent
# Postgres connection, as a DSN in an existing Secret:
# postgres://user:password@host:5432/terdut?sslmode=require
#
# The chart provisions no database. In this cluster the wrapper chart declares an
# acid.zalan.do postgresql CR and points this at the Secret the operator writes;
# anywhere else, any reachable Postgres will do.
# Postgres connection. The chart provisions no database; it expects one to exist.
database:
dsnSecret:
# Required. A DSN with no password in it:
# postgres://terdut@terdut-postgres:5432/terdut?sslmode=require
#
# The password is deliberately a separate setting. pgx falls back to libpq's
# environment variables for anything the DSN omits, so PGPASSWORD supplies it
# without the credential appearing in values, in the rendered manifest, or in
# `kubectl describe pod`.
dsn: ""
# Where PGPASSWORD comes from. With the Zalando postgres operator this is the
# Secret it generates for the role — `<user>.<cluster>.credentials.postgresql.acid.zalan.do`,
# whose keys are `username` and `password` — so a from-scratch rebuild mints a
# new password and the server picks it up with nothing to keep in sync.
#
# Read at process start only: rotating the password needs a pod restart.
#
# Leave name empty only if the DSN carries its own password, which puts it in
# the manifest.
passwordSecret:
name: ""
key: dsn
key: password
service:
type: ClusterIP