Skip to content

Support sslcert/sslkey client certificate authentication for PostgreSQL - #441

Merged
tianzhou merged 2 commits into
mainfrom
claude/verdict-issue-439-cmg9zq
Sep 28, 2026
Merged

tianzhou merged 2 commits into
mainfrom
claude/verdict-issue-439-cmg9zq

Conversation

@tianzhou

Copy link
Copy Markdown
Member

Closes bytebase/dbhub#439

Problem

PostgreSQL servers that enforce certificate authentication in pg_hba.conf (hostssl ... cert, clientcert=verify-ca / verify-full) reject DBHub with FATAL: connection requires a valid client certificate. The DSN parser only read sslmode and sslrootcert; the standard libpq sslcert / sslkey parameters were silently dropped and the TOML config had no matching fields.

Change

sslcert and sslkey are now accepted as DSN query parameters and as TOML [[sources]] fields, mirroring how sslrootcert works:

postgres://user:pass@host:5432/db?sslmode=require&sslcert=~/.certs/client.crt&sslkey=~/.certs/client.key
[[sources]]
id = "dev"
type = "postgres"
host = "db.example.internal"
database = "app"
user = "app"
password = "${PG_PASSWORD}"
sslmode = "verify-ca"
sslrootcert = "~/.certs/ca.crt"
sslcert = "~/.certs/client.crt"
sslkey = "~/.certs/client.key"

Rules

  • PostgreSQL only.
  • Both must be set, or neither.
  • sslmode must be require, verify-ca or verify-full. libpq sends the client cert in every SSL mode, but node-postgres only negotiates TLS in these modes, so a cert with disable / no sslmode would be silently ignored. It is rejected instead.
  • Independent of sslrootcert: require + client cert authenticates the client without verifying the server.
  • PEM only, unencrypted. An encrypted key is rejected at startup with a clear message rather than Node's opaque decoder error. sslpassword is left for a follow-up.

Connector (src/connectors/postgres/index.ts): reads the two params, passes the PEM contents to node-postgres as TLS cert/key on top of whatever the sslmode branch already built (require keeps rejectUnauthorized: false; verify-* keep CA handling). Validation lives in the parser as well as the TOML loader so --dsn / DSN env get the same checks. The ~/ expansion and FailedToReadCertificate wrapping are shared across all three SSL files.

TOML loader (src/config/toml-loader.ts): new fields with type / both-or-neither / sslmode / file-readable validation; ~/ expansion; backfill from DSN; DSN-vs-field conflict check; and DSN building. The sslrootcert file check and DSN-param emission were pulled into shared helpers (validateReadableFile, postgresSslFileParams) rather than copied three times.

Docs: docs/config/toml.mdx, docs/config/command-line.mdx, dbhub.toml.example, CLAUDE.md.

Tests

  • dsn-parser.test.ts: cert/key under each TLS mode, alongside sslrootcert, ~ expansion, one-without-the-other, disable / unset sslmode, missing files, encrypted PKCS#8 and legacy PEM keys.
  • toml-loader.test.ts: accepted with each TLS mode and with sslrootcert, rejected for MySQL / one-without-the-other / disable / unset / missing file / directory, DSN backfill, DSN conflict and ~-equal non-conflict, DSN merge and build with percent-encoding and no duplication.
  • New postgres-client-cert.integration.test.ts: generates a CA + server + client cert with openssl, boots postgres:15-alpine with ssl=on and hostssl all all all cert, and checks that a cert connection succeeds (pg_stat_ssl.client_dn contains CN=testuser) under both verify-ca and require, and that a cert-less connection is refused. Skipped if openssl is not installed.

Verified locally: pnpm test:unit (1150 passed), pnpm run build. The container I worked in has no Docker, so the integration suite is validated by CI on this PR rather than locally.

Out of scope

  • sslpassword (encrypted keys)
  • Client certificates for MySQL / MariaDB / SQL Server
  • libpq-style key file permission checks (would break existing setups; documented instead)

🤖 Generated with Claude Code

https://claude.ai/code/session_012UzToxWXzC7oQ9xMnhM192


Generated by Claude Code

PostgreSQL servers that enforce certificate authentication in pg_hba.conf
(`hostssl ... cert`, `clientcert=verify-*`) reject DBHub with "connection
requires a valid client certificate" because the DSN parser only read
sslmode and sslrootcert. Add the standard libpq `sslcert` / `sslkey`
parameters, accepted both as DSN query params and as TOML source fields.

Connector (src/connectors/postgres/index.ts):
- Read sslcert/sslkey and pass the PEM contents to node-postgres as TLS
  `cert`/`key`, on top of whatever the sslmode branch already built
  (require keeps rejectUnauthorized=false; verify-* keep CA handling).
- Fail fast on an inconsistent DSN: one of the pair without the other, or
  a client cert with sslmode=disable / unset (node-postgres would silently
  drop it on a plaintext connection). Encrypted PEM keys are rejected with
  a clear message instead of Node's opaque decoder error.
- Share the ~/ expansion + FailedToReadCertificate wrapping across all
  three SSL files.

TOML loader (src/config/toml-loader.ts):
- New sslcert/sslkey fields: PostgreSQL only, both-or-neither, sslmode in
  require/verify-ca/verify-full, file must exist and be readable.
- ~/ expansion, backfill from DSN, DSN/field conflict check, and DSN
  building all mirror sslrootcert via shared helpers.

Tests cover the parser and loader rules, plus a new Testcontainers suite
that boots Postgres with `hostssl all all all cert` and verifies a cert
connection succeeds (client_dn = CN=user) and a cert-less one is refused.

Docs: docs/config/toml.mdx, docs/config/command-line.mdx,
dbhub.toml.example, CLAUDE.md.

Closes #439

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012UzToxWXzC7oQ9xMnhM192
Copilot AI lite review requested due to automatic review settings September 28, 2026 15:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

Only minor documentation and comment alignment nits remain; no blocking issues were identified.

Review effort: Lite
Findings: 2 Low severity

Open (2)
What changed in this PR

Adds PostgreSQL client certificate authentication through sslcert/sslkey DSN parameters and TOML settings.

Changes:

  • Adds validation, PEM loading, TLS integration, and encrypted-key detection.
  • Extends TOML parsing, DSN generation, and configuration types.
  • Adds unit/integration tests and documentation updates.
File Description
src/​types/​config.ts Adds certificate configuration fields.
src/​connectors/​postgres/​index.ts Loads and applies client certificates.
src/​connectors/​postgres/​failed-to-read-certificate.ts Expands certificate error handling.
src/​connectors/​__tests__/​postgres-client-cert.integration.test.ts Tests PostgreSQL certificate authentication.
src/​connectors/​__tests__/​dsn-parser.test.ts Tests DSN certificate parsing and validation.
src/​config/​toml-loader.ts Validates and merges certificate settings.
src/​config/​__tests__/​toml-loader.test.ts Tests TOML certificate configuration.
docs/​config/​toml.mdx Documents TOML certificate settings.
docs/​config/​command-line.mdx Documents DSN parameters.
dbhub.toml.example Adds certificate configuration examples.
CLAUDE.md Updates PostgreSQL configuration guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread CLAUDE.md Outdated
Comment thread src/config/toml-loader.ts Outdated
…riction

Address review: CLAUDE.md and the postgresSslFileParams comment said
client certificates apply in "any TLS mode", but disable and an unset
sslmode are rejected. Name the three accepted modes explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012UzToxWXzC7oQ9xMnhM192
@tianzhou
tianzhou merged commit f16dc5e into main Sep 28, 2026
3 checks passed
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.

Support sslcert / sslkey (client certificate authentication) for PostgreSQL

3 participants