From cc31c993dd2f4e93705fdd3dbf3be413e31b4a67 Mon Sep 17 00:00:00 2001 From: Niklas Ye Date: Sun, 20 Sep 2026 11:00:05 +0200 Subject: [PATCH] Take the database password from PGPASSWORD, not the DSN 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. --- Makefile | 11 ++++---- README.md | 17 +++++++---- .../terdut-server/templates/deployment.yaml | 14 ++++++---- charts/terdut-server/values.yaml | 28 +++++++++++++------ 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index 59c5a12..fa2ec48 100644 --- a/Makefile +++ b/Makefile @@ -87,11 +87,12 @@ fmt: ## Report unformatted files echo "gofmt needed:"; echo "$$unformatted"; gofmt -d .; exit 1; \ fi -# database.dsnSecret.name has no default and the deployment `required`s it: the -# chart provisions no database and cannot guess where the credentials live, so a -# render without it is meant to fail. Setting it here keeps the lint honest about -# what a working install needs. -HELM_LINT_SET = --set image.tag=v0.0.0 --set database.dsnSecret.name=terdut-db +# database.dsn has no default and the deployment `required`s it: the chart +# provisions no database and cannot guess where it is, so a render without it is +# meant to fail. Setting it here keeps the lint honest about what a working +# install needs. +HELM_LINT_SET = --set image.tag=v0.0.0 \ + --set 'database.dsn=postgres://terdut@terdut-postgres:5432/terdut?sslmode=require' .PHONY: helm-lint helm-lint: ## Lint and render the chart diff --git a/README.md b/README.md index 18c3dcf..91daf23 100644 --- a/README.md +++ b/README.md @@ -119,17 +119,24 @@ than an `Ingress`. TLS is terminated at the gateway, so the server itself never | `networking.listener` | `""` | Gateway listener (`sectionName`) to bind to. Empty attaches to every matching listener, **including plaintext HTTP** — set it to the HTTPS listener's name to serve TLS only | | `networking.servicePort` | `8080` | Port the route forwards to; keep in sync with `service.port` | | `bootstrap.enabled` | `true` | Runs a post-install hook that creates the first user and stores its API key in the `-admin-key` Secret. Already-bootstrapped servers are left alone | -| `database.dsnSecret.name` | `""` | **Required.** Existing Secret holding the Postgres DSN. The chart provisions no database | -| `database.dsnSecret.key` | `dsn` | Key within that Secret | +| `database.dsn` | `""` | **Required.** Postgres DSN, with no password in it. The chart provisions no database | +| `database.passwordSecret.name` | `""` | Secret supplying `PGPASSWORD`. With the Zalando postgres operator, the Secret it generates for the role | +| `database.passwordSecret.key` | `password` | Key within that Secret | The API key travels in an `Authorization: Bearer` header, so set `networking.listener` whenever the hostname is reachable outside a trusted network. #### The database -The chart provisions no database: it takes a DSN from a Secret and expects a Postgres that already -exists. In this cluster the wrapper chart declares an `acid.zalan.do/v1 postgresql` CR and passes -the Secret the operator writes; anywhere else, any reachable Postgres 14+ will do. +The chart provisions no database: it takes a DSN and expects a Postgres that already exists. In this +cluster the wrapper chart declares an `acid.zalan.do/v1 postgresql` CR; anywhere else, any reachable +Postgres 14+ will do. + +The DSN carries no password. pgx falls back to libpq's environment variables for whatever the DSN +leaves out, so the password arrives as `PGPASSWORD` from a Secret and never appears in values, in +the rendered manifest or in `kubectl describe pod`. With the postgres operator that Secret is the +one it generates for the role, so a rebuild mints a new password with nothing to keep in sync — +the same wiring miniflux uses. The server migrates its own schema on startup, so a new database only has to exist and be writable. diff --git a/charts/terdut-server/templates/deployment.yaml b/charts/terdut-server/templates/deployment.yaml index d73326c..1d3d785 100644 --- a/charts/terdut-server/templates/deployment.yaml +++ b/charts/terdut-server/templates/deployment.yaml @@ -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 diff --git a/charts/terdut-server/values.yaml b/charts/terdut-server/values.yaml index 81f3f0a..d5f51df 100644 --- a/charts/terdut-server/values.yaml +++ b/charts/terdut-server/values.yaml @@ -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 — `..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