Skip to content

Reload the PostgreSQL client certificate on each connection - #111

Open
arpitjain099 wants to merge 1 commit into
smallstep:masterfrom
arpitjain099:fix/postgresql-reload-client-cert
Open

Reload the PostgreSQL client certificate on each connection#111
arpitjain099 wants to merge 1 commit into
smallstep:masterfrom
arpitjain099:fix/postgresql-reload-client-cert

Conversation

@arpitjain099

Copy link
Copy Markdown

Fixes #110.

Confirmed the mechanism before changing anything. pgx.ParseConfig reads sslcert/sslkey at parse time and leaves the result in the TLS config that every later connection uses:

TLSConfig != nil: true
Certificates: 1, GetClientCertificate set: false

Open parses once and hands that config to pgxstdlib.OpenDB, so a certificate rotated afterwards is never looked at again.

This installs a GetClientCertificate callback, which crypto/tls invokes per handshake, and clears Certificates so the stale copy is not reachable at all.

The one design choice worth explaining: the callback calls pgx.ParseConfig(dataSourceName) again rather than reading the two files directly. Reading them directly would mean re-implementing how libpq resolves them — URL versus keyword DSN forms, PGSSLCERT/PGSSLKEY, and anything else pgx honours — and keeping that second implementation in step. Going back through ParseConfig keeps one interpretation of the DSN. It costs a parse per handshake, which is rare next to queries on a pooled connection.

sslmode=prefer and friends leave the non-TLS attempt in Fallbacks with its own TLSConfig, so those get the same treatment.

Tests

Three cases in a new postgresql_tls_test.go, none of which need a PostgreSQL server:

  • rotation: write cert-A, wire it up, assert the handshake would offer cert-A; overwrite the files with cert-B as a CSI volume would, assert it now offers cert-B
  • the stale copy is cleared: asserts ParseConfig really did load one certificate, then that it is gone afterwards, so a later change preferring Certificates over the callback cannot quietly reintroduce this
  • no client certificate: a DSN without sslcert/sslkey is untouched, for sslmode=require and sslmode=disable

Against the current behaviour the first two fail with no GetClientCertificate callback installed and expected the eagerly loaded certificate to be cleared, got 1.

go build ./..., go vet ./postgresql/, gofmt and go test ./... are clean, and go build -tags nopgx ./... still builds.

What I have not done is exercise a real rotation against a live PostgreSQL with mutual TLS; the tests assert on what the handshake would be handed rather than on a completed connection. If you would like that as an integration test I am happy to add it, though it needs a server with client-cert auth in CI.

ParseConfig reads sslcert and sslkey once and stores the result in
TLSConfig.Certificates, and that config backs every connection the pool
opens afterwards. Where the certificate is rotated underneath a running
process, as with a cert-manager CSI volume, connections established after
the original expires are still offered the expired certificate and the
server rejects them, even though the files on disk hold a valid one. The
only way out today is to restart the process.

Install a GetClientCertificate callback instead, which crypto/tls calls
per handshake, and clear the eagerly loaded copy so it cannot be reached.

The callback goes back through ParseConfig rather than reading the files
itself, so the DSN keeps being interpreted the way libpq does, including
the keyword and URL forms and the PGSSLCERT and PGSSLKEY environment
variables, without a second implementation to keep in step.

sslmode=prefer and friends leave the non-TLS attempt in Fallbacks with
their own TLSConfig, so those are covered too. A DSN with no client
certificate is left alone.

Fixes smallstep#110

Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
@arpitjain099
arpitjain099 requested a review from a team as a code owner August 30, 2026 22:55
@CLAassistant

CLAassistant commented Aug 30, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PostgreSQL backend does not reload rotated client TLS certificates

3 participants