Skip to content

fix: ssl_mode=require/verify-ca/verify-full silently allows plaintext - #45

Merged
aesslinger merged 1 commit into
mainfrom
fix/ssl-mode-require-plaintext-fallback
Aug 19, 2026
Merged

fix: ssl_mode=require/verify-ca/verify-full silently allows plaintext#45
aesslinger merged 1 commit into
mainfrom
fix/ssl-mode-require-plaintext-fallback

Conversation

@aesslinger

@aesslinger aesslinger commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

build_pool never called cfg.ssl_mode(...) on the deadpool_postgres::Config. Left as None, the underlying tokio_postgres::Config defaults to SslMode::Prefer (confirmed in tokio-postgres-0.7.18/src/config.rs:255) — negotiate TLS if offered, silently accept plaintext otherwise. So ssl_mode=require/verify-ca/verify-full built the right rustls verifier (that's what #35/#37/#40 fixed) but never actually enforced TLS at the protocol level.

This is security-relevant, not just a correctness bug: a user opting into "TLS or nothing" got an unencrypted connection with no error if the server couldn't/wouldn't negotiate TLS.

Fixes #43.

Discovery context

Found during a post-merge sanity pass on main after #35/#37/#40/#41/#42 landed. Confirmed via git log/git show that this predates all six recently-merged PRs — present since client.rs was first staged in #3, not a regression from any of them.

Fix

Added resolve_ssl_mode, mapping this plugin's ssl_mode strings to deadpool_postgres::SslMode, matching the builtin driver's build_postgres_configurations (src-tauri/src/pool_manager.rs) mapping exactly:

  • disableDisable
  • allow/preferPrefer (no behavior change — already tokio_postgres's default)
  • require/verify-ca/verify-fullRequire (the actual fix)
  • unset/unknown → unmapped (no behavior change)

Wired into build_pool right after cfg.manager = Some(...).

Testing — exact local setup and results against a real SSL-enabled server

Automated tests only prove the mapping function is correct and that the bug reproduces against a non-SSL server. To be sure the fix doesn't break real TLS connections, I stood up a second local instance with actual TLS enabled and drove the built binary against it directly, alongside the existing non-SSL fixture.

Setup (both via Podman, run locally, not part of the repo or CI):

  • Port 54320: plain postgres:16, no SSL — matches CI's live-db-integration fixture exactly.
  • Port 55432: fresh postgres:16 with a real self-signed cert generated via openssl req -x509 -newkey rsa:2048, ssl = on set in postgresql.conf, restarted, confirmed via SHOW ssl;on.

Manual repro against the built binary, piping raw JSON-RPC requests into ./target/debug/postgresql-plugin, run against the fix (current code):

ssl_mode Target Expected Actual result
disable non-SSL (54320) connects (plaintext) {"id":1,"jsonrpc":"2.0","result":null}
unset non-SSL (54320) connects (default Prefer, unchanged) {"id":1,"jsonrpc":"2.0","result":null}
prefer SSL-on (55432) connects (opportunistic TLS) {"id":1,"jsonrpc":"2.0","result":null}
require non-SSL (54320) fails — this is the fix {"error":{"code":-32603,"message":"Connection failed: ... error performing TLS handshake"},...}
require SSL-on (55432) connects (real TLS handshake succeeds) {"error":{"code":-32603,"message":"... error performing TLS handshake"}} ❌ — see "Related finding" below

Before/after on the actual bug (require against the non-SSL server) — done via the automated live-DB test, not a second manual binary run: temporarily commented out the one new line (cfg.ssl_mode = resolve_ssl_mode(...)) in client.rs, rebuilt, ran the new test — confirmed ssl_mode_require_fails_against_a_server_without_tls fails against that reverted code (connects over plaintext, matching the original bug). Restored the line, rebuilt, confirmed the test passes.

Automated tests:

  • Unit tests (resolve_ssl_mode_* in client_tests.rs): every mapping case, written first and confirmed failing to compile before the function existed.
  • Live-DB regression test (ssl_mode_require_fails_against_a_server_without_tls in tests/live_db.rs): runs against CI's real postgres:16 fixture (no SSL). Confirmed Red before the fix, Green after (see above).

Related finding, filed separately (explains the one row above that didn't pass)

Row 5 above (require against the real SSL-enabled server) failed with a TLS handshake error — expected to succeed, since require should accept any cert without validating it. Root-caused: build_tls_connector's needs_cert_validation check only matches verify-ca/verify-full, so require falls through to the final with_platform_verifier() branch, which does validate against the OS trust store — contradicting the function's own doc comment ("require forces TLS without certificate validation") and the builtin's actual behavior (NoCertVerifier, no validation at all for require). Confirmed via git show 9986b1e that this predates every PR from this session, including this one — a second, distinct, pre-existing bug (different root cause: cert validation vs. protocol-level enforcement) found only because I tested this fix against a real TLS server rather than stopping at the non-SSL repro. Filed as #44, intentionally out of scope here.

Test plan

  • cargo test --lib --bins — 111/111 pass (107 previous + 4 new)
  • cargo test --test live_db (against a live non-SSL PostgreSQL 16 instance) — 8/8 pass, including the new regression test, verified Red-then-Green against a temporarily reverted client.rs
  • Manual repro against real non-SSL and real self-signed-cert SSL-enabled PostgreSQL instances (exact commands/results above)
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • cargo build --release — clean
  • npx markdownlint CHANGELOG.md — clean

build_pool never called cfg.ssl_mode(...) on the deadpool_postgres
Config, so the underlying tokio_postgres::Config kept its own default
(SslMode::Prefer: negotiate TLS if offered, but accept plaintext
otherwise) regardless of what this plugin's ssl_mode was actually set
to. A user opting into "TLS or nothing" got an unencrypted connection
with no error if the server couldn't or wouldn't negotiate TLS -- a
security-relevant gap, not just a correctness one.

Added resolve_ssl_mode, mapping this plugin's ssl_mode strings to
deadpool_postgres::SslMode to match the builtin driver's
build_postgres_configurations mapping exactly (disable->Disable,
allow/prefer->Prefer, require/verify-ca/verify-full->Require), and
wired it into build_pool.

Proved the bug and the fix with a live-database test against a real
non-SSL PostgreSQL instance (matches CI's postgres:16 fixture, which
also runs without SSL): confirmed the new test fails against the
pre-fix code (ssl_mode=require connects successfully over plaintext)
and passes after the fix. Also manually verified disable/prefer/unset
modes against both a non-SSL and a self-signed-cert SSL-enabled
instance to confirm no regression in the modes this change doesn't
touch.

Separately found (while testing this fix against the SSL-enabled
instance) that ssl_mode=require validates the server cert against the
platform trust store instead of skipping validation entirely -- a
distinct, pre-existing bug in build_tls_connector unrelated to this
one. Filed as #44, left out of scope here.

Fixes #43.
@github-actions

Copy link
Copy Markdown

Version suggestion

Based on this PR's title (fix) and the prerelease:beta label:

Current 1.0.0-beta.7
Suggested next tag v1.0.0-beta.8

This is informational only — no tag or release is created automatically yet.

@aesslinger aesslinger self-assigned this Aug 19, 2026
@aesslinger
aesslinger merged commit 82d35cc into main Aug 19, 2026
27 of 28 checks passed
@aesslinger
aesslinger deleted the fix/ssl-mode-require-plaintext-fallback branch August 19, 2026 14:50
aesslinger added a commit that referenced this pull request Aug 19, 2026
build_tls_connector's own doc comment already said require should
force TLS "without certificate validation", but needs_cert_validation
only matched verify-ca/verify-full -- require fell through to the
final with_platform_verifier() fallback, which does validate against
the OS trust store, defeating the entire point of require vs.
verify-full (the standard require use case is self-signed certs or
private CAs the user hasn't configured ssl_ca for).

Ported NoCertVerifier from the builtin driver's
src-tauri/src/pool_manager.rs::NoCertVerifier -- accepts any
certificate unconditionally, including bypassing TLS 1.2/1.3 signature
verification, not just chain/hostname checks (more permissive than
VerifyCaCertVerifier's chain-only bypass, but that's the builtin's own
deliberate choice for this mode, not something to improve on
silently). Routed require mode to it, threading client_auth through
the same pattern already used by the verify-ca/verify-full branches so
mTLS + require still work together.

Proved the bug and the fix live against a real self-signed-cert
SSL-enabled PostgreSQL instance (separate from the non-SSL fixture
used for #43): require failed the TLS handshake before this fix, and
now connects successfully. Confirmed no regression in verify-ca/
verify-full (still correctly validate -- existing VerifyCaCertVerifier
unit tests unaffected) or require against a non-SSL server (still
correctly fails, per #43/#45).

Separately found while testing this fix that verify-ca without an
explicit ssl_ca file silently falls back to platform-trust validation
instead of erroring like the builtin does -- a distinct, smaller
discrepancy, filed as #46 and left out of scope here.

Fixes #44.
aesslinger added a commit that referenced this pull request Aug 19, 2026
Continuation of the beta line after PRs #45/#47/#48 (ssl_mode=require/
verify-ca/verify-full silently allowing plaintext, ssl_mode=require
validating against the platform trust store instead of skipping
validation, and verify-ca silently falling back to platform trust
instead of requiring an explicit CA file).

Also corrected three README claims that had gone stale across this
session's TLS work: the ssl_mode value list was missing allow/prefer
in two places, ssl_ca's description didn't note it's now required
(not just optional) for verify-ca, and the pool-caching description
still said host:port:database:user, missing startup_script and every
TLS param folded in by #37.

Verified: .tabularium re-validated clean against the live registry
schema; cargo build/test (114/114)/clippy/fmt all pass; markdownlint
clean; manual live-TLS smoke tests against real non-SSL and
self-signed-cert SSL-enabled PostgreSQL instances covering every
ssl_mode value and error path all behave as documented.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prerelease:beta Version suggestion targets a beta prerelease

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ssl_mode=require/verify-ca/verify-full silently falls back to plaintext (deadpool Config.ssl_mode never set)

1 participant