Skip to content

fix(postgres): follow libpq sslmode semantics instead of always verifying - #25

Merged
vsdudakov merged 1 commit into
mainfrom
fix/postgres-sslmode-verification
Aug 31, 2026
Merged

fix(postgres): follow libpq sslmode semantics instead of always verifying#25
vsdudakov merged 1 commit into
mainfrom
fix/postgres-sslmode-verification

Conversation

@vsdudakov

Copy link
Copy Markdown
Owner

The bug

Every sslmode except disable verified the server certificate against the OS
trust store:

let mgr = if pg_config.get_ssl_mode() == SslMode::Disable {
    Manager::from_config(pg_config, NoTls, mgr_config)
} else {
    Manager::from_config(pg_config, make_tls_connector()?, mgr_config)  // roots + hostname
};

That makes prefer — the default, and what a URL inherits when it says nothing
about TLS — unable to reach a managed PostgreSQL at all. Amazon RDS/Aurora,
Cloud SQL and Azure all present a certificate signed by the provider's own
private CA, which no OS trust store carries, so the connection dies in the
handshake with UnknownIssuer before the first query:

DBConnectionError: connection error: Error occurred while creating a new object:
error performing TLS handshake

libpq — and therefore asyncpg and psycopg, which callers migrate from —
authenticates the server only under verify-ca and verify-full. prefer and
require encrypt and nothing more; require promises TLS, not identity. A
codebase swapping asyncpg for yara-orm keeps its URLs and loses its database.

The fix

prefer and require now use a verifier that accepts the presented
certificate. The connection is still TLS, and the handshake signature is still
checked against that certificate's key — only the question of whose
certificate it is goes unasked, exactly as under libpq.

Verification stays available and gains the spelling libpq uses for it:

sslmode TLS Certificate checked
disable no
prefer (default) yes, plaintext if unsupported no
require yes no
verify-ca yes chain must reach a trusted root
verify-full yes chain and hostname

sslrootcert=<path> adds a PEM file's roots to the trust store for the
verifying modes — a provider's CA bundle, typically. As in libpq it does not by
itself turn verification on; an unreadable file, or one holding no certificate,
is a configuration error rather than a silent fallback to the OS roots.
tokio-postgres parses neither verify-* spelling nor sslrootcert, so both are
consumed by the engine and the driver is handed sslmode=require.

verify-ca exists for reaching a database through a name its certificate does
not carry — a proxy endpoint, a tunnel, a bare IP — so it skips only the
hostname check, by wrapping WebPkiServerVerifier and translating
NotValidForName into acceptance. Everything else about the chain still holds.

Verification

Against a local PostgreSQL 16 with TLS on and a private CA, checking
pg_stat_ssl for each connection that succeeds:

Case Result
prefer (default), private CA connected, ssl=t
require, private CA connected, ssl=t
verify-full, CA untrusted refused
verify-ca, CA untrusted refused
verify-full + sslrootcert connected, ssl=t
verify-ca + sslrootcert connected, ssl=t
disable connected, ssl=f
verify-full + unreadable sslrootcert config error naming the file
plaintext-only server, prefer connected (falls back)
cert whose SAN misses the dialled host, verify-ca connected
cert whose SAN misses the dialled host, verify-full refused

Suite: 1996 passed, 2919 skipped (PostgreSQL; the skips are the backends not
running locally), and 61 passed in cargo test --lib, six of them new around
the URL parsing, the sslrootcert error path and connector construction at each
verification level.

Release

Version bumped to 1.17.0 across Cargo.toml, pyproject.toml and
python/yara_orm/__init__.py, with a CHANGELOG.md entry, so this can be cut
as a release. It is a behaviour change for anyone who was relying on require
to verify — they want verify-full now — hence the minor rather than a patch.

Unrelated observation

The published 1.16.0 wheel fails the handshake against an RSA server
certificate even when its CA is trusted (decrypt_error, i.e. BadSignature);
ECDSA certificates are fine. Neither a debug nor a release build from this
source reproduces it, so it looks like the dependency set that wheel was built
against — there is no committed Cargo.lock. Worth a look before the next
release, separately from this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PrJLyTdr7CMvrzxENq3NfP

…ying

Every sslmode except `disable` verified the server certificate against the OS
trust store. That made `prefer` — the default, and what a URL inherits when it
says nothing — unable to reach a managed database at all: RDS/Aurora, Cloud SQL
and Azure present a certificate signed by the provider's own private CA, which
no OS trust store carries, so the handshake failed with UnknownIssuer before the
first query. libpq, and therefore the asyncpg/psycopg callers this ORM is a drop
-in for, authenticates the server only under `verify-ca` and `verify-full`;
`prefer` and `require` encrypt and nothing more.

`prefer` and `require` now use a verifier that accepts the presented
certificate. The connection is still TLS — the handshake signature is still
checked against that certificate's key — and only the question of who the
certificate belongs to goes unasked, exactly as under libpq.

Verification stays available, and gains the spelling libpq uses for it:
`sslmode=verify-ca` (chain) and `sslmode=verify-full` (chain and hostname), with
`sslrootcert=<path>` to add a provider's CA bundle to the trust store.
tokio-postgres parses neither spelling, so both are consumed here and handed to
the driver as `sslmode=require`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PrJLyTdr7CMvrzxENq3NfP
@vsdudakov
vsdudakov merged commit d2dc791 into main Aug 31, 2026
4 checks passed
@vsdudakov
vsdudakov deleted the fix/postgres-sslmode-verification branch August 31, 2026 18:14
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.

1 participant