feat: per-user tls_client_certificate_required - #1283
Draft
bhargavtheertham-cb wants to merge 1 commit into
Draft
Conversation
Setting `tls_client_ca_certificate` makes client certificates mandatory
for every TLS connection, because rustls' default client verifier denies
anonymous clients. On a listener serving both mTLS users (`identity`) and
password users, that leaves the password users unable to negotiate TLS at
all: they get a `certificate_required` alert, so their only working option
is a plaintext connection.
PgDog cannot decide this during the handshake, which completes before the
startup packet carrying `user=` arrives. At that point it has no idea who
is connecting.
Build the verifier with `allow_unauthenticated()` so a certificate is
verified when presented but its absence no longer fails the handshake, and
move the requirement to authentication, where the user is known. A new
per-user `tls_client_certificate_required` defaults to true and is enforced
only when a client CA is configured and the client connected over TLS:
- without a client CA no certificate is ever requested, so requiring one
could never be satisfied;
- plaintext connections stay governed by `tls_client_required`.
Behavior is unchanged at the default. A user connecting over TLS without a
certificate is still rejected, now with `NoClientCertificate` at the auth
layer rather than a TLS alert. mTLS is unaffected: users with `identity`
are still rejected by the existing `tls_identity()` comparison, and an
entry with neither identity nor password still fails closed via
`NoPasswordConfig`. Certificates that are presented are verified against
the CA bundle exactly as before; only the empty-certificate case changes.
Track certificate presence on `Stream` separately from `tls_identity`,
which is `None` both when no certificate was sent and when one was sent
without a dNSName SAN or Subject CN.
Co-Authored-By: Claude <noreply@anthropic.com>
bhargavtheertham-cb
marked this pull request as draft
July 29, 2026 22:28
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Setting
tls_client_ca_certificatemakes client certificates mandatory for every TLS connection, because rustls' default client verifier denies anonymous clients. On a listener serving both mTLS users (identity) and password users (password/password_hash), that leaves the password users unable to negotiate TLS at all — they get acertificate_requiredalert, so their only working option is a plaintext connection:PgDog can't resolve this during the handshake, which completes before the startup packet carrying
user=arrives — at that point it has no idea who is connecting. So the choice is currently all-or-nothing for every connection on the listener.Worth noting the current requirement doesn't actually protect those users: they're already reachable unauthenticated over plaintext. It only forces the legitimate ones onto the unencrypted path.
Change
Build the verifier with
allow_unauthenticated()so a certificate is verified when presented but its absence no longer fails the handshake, and move the requirement to authentication, where the user is known.A new per-user
tls_client_certificate_requireddefaults totrueand is enforced only when a client CA is configured and the client connected over TLS:with_no_client_auth), so requiring one could never be satisfied — enforcing it unconditionally would reject every user in every deployment that doesn't configure client CAs;tls_client_required.Backwards compatibility
Behavior is unchanged at the default:
NoClientCertificate)tls_client_requiredThe one visible difference at default is that a certless client is now rejected after the handshake rather than during it, so the error is a Postgres auth failure instead of a TLS alert. That also means slightly more work per rejected connection — happy to gate the verifier change behind a config flag instead if you'd prefer to keep the early rejection.
mTLS is unaffected
Identity enforcement never lived in the handshake —
client/mod.rsalready requiresstream.tls_identity() == Some(identity)for any user with a configured identity, and a certless client getsNone, so it still fails withNoIdentity. An entry with neither identity nor password still fails closed via the existingNoPasswordConfigguard. Certificates that are presented are verified against the CA bundle exactly as before; only the empty-certificate case changes.Notes
Streamseparately fromtls_identity, which isNoneboth when no certificate was sent and when one was sent without a dNSName SAN or Subject CN..schema/users.schema.jsonregenerated viacargo run -p pgdog-jsonschema.[general]fallback with a per-user override in the style ofpooler_mode, if you'd prefer that shape.Testing
cargo fmt --all -- --checkandcargo clippy --all-targets -- -D warningscleancargo test -p pgdog-config— 83 passedclient_cert_verifier_allows_anonymous_clientsassertsoffer_client_auth()stays true whileclient_auth_mandatory()becomes false; two config tests cover the unset and opted-out casesI wasn't able to run the full
pgdoglib suite locally (it needs the Postgres fixtures fromintegration/setup.sh), so CI is the first full run.