Skip to content

Add server-side TLS (pg_doorman → PostgreSQL)#208

Open
vadv wants to merge 12 commits intomasterfrom
feature/server-side-tls
Open

Add server-side TLS (pg_doorman → PostgreSQL)#208
vadv wants to merge 12 commits intomasterfrom
feature/server-side-tls

Conversation

@vadv
Copy link
Copy Markdown
Collaborator

@vadv vadv commented Apr 23, 2026

Summary

  • Encrypted connections from pg_doorman to PostgreSQL servers with five SSL modes: disable, prefer (default), require, verify-ca, verify-full
  • Mutual TLS support — pg_doorman presents client certificate to PostgreSQL
  • TLS-aware cancel requests — cancel uses TLS when the main connection used TLS
  • Per-pool TLS configuration with global defaults in [general]

Breaking config change

Old non-functional fields server_tls and verify_server_certificate are removed. They were parsed but never worked — stream.rs returned an error when the server accepted TLS. Replacement: server_tls_mode (string) + server_tls_ca_cert, server_tls_certificate, server_tls_private_key (paths).

Configuration example

[general]
server_tls_mode = "verify-ca"
server_tls_ca_cert = "/etc/ssl/pg-ca.crt"
server_tls_certificate = "/etc/ssl/doorman.crt"
server_tls_private_key = "/etc/ssl/doorman.key"

[pools.production]
# inherits server_tls_mode from general

[pools.legacy]
server_tls_mode = "disable"  # per-pool override

Test plan

  • 474 unit tests pass
  • cargo fmt clean
  • cargo clippy -- --deny "warnings" clean
  • BDD: 6 scenarios (make test-bdd TAGS=@server-tls) — prefer, require, verify-ca, verify-ca-wrong-CA, disable, per-pool override
  • CI pipeline

🤖 Generated with Claude Code

dmitrivasilyev added 12 commits April 23, 2026 17:32
Covers TLS for pg_doorman-to-PostgreSQL connections: five SSL modes
(disable/prefer/require/verify-ca/verify-full), mutual TLS, TLS-aware
cancel requests, and per-pool configuration with global defaults.
What was needed: A foundational enum representing the five TLS modes for
backend (server-facing) connections — disable, prefer, require, verify-ca,
verify-full — so that later tasks can wire up server-side TLS config and
connection logic.

Summary: Clients can now refer to ServerTlsMode when connecting to PostgreSQL
backends. The enum defaults to Prefer, parses from/renders to the canonical
lowercase string forms, and exposes three helper predicates: requires_ca,
sends_ssl_request, and requires_tls.
Что требовалось: нужна структура, инкапсулирующая режим TLS и готовый коннектор для исходящих соединений с сервером БД.
Суть: пул теперь может получить предсобранный TlsConnector при загрузке конфигурации, вместо того чтобы строить его при каждом подключении.
The old boolean fields were parsed by serde but the TLS connection code
returned an error when the server accepted TLS, making them non-functional.

Replace with string-based server_tls_mode (prefer/require/disable/verify-full)
and optional certificate path fields (server_tls_ca_cert, server_tls_certificate,
server_tls_private_key). Pool fields are Option<String> to inherit from General
via unwrap_or(). Server backend uses false/false stubs pending full TLS wiring.
What was needed: server-facing TLS fields had no validation, so misconfigured cert paths, mismatched cert/key pairs, and missing CA certs for verify modes would only fail at runtime.
Summary: Startup now rejects invalid server TLS configurations eagerly and logs the effective server TLS mode and cert paths alongside the existing frontend TLS info.
Each Address now carries a resolved Arc<ServerTlsConfig> built once at
pool creation time, so Server::startup() can consume it directly without
re-reading global config on every connection.

build_server_tls_for_pool() merges pool-level TLS overrides with general
defaults. Dynamic pools (auth_query passthrough) call the same helper.
Test construction sites use struct-update syntax to stay forward-compatible.
What was needed: server-side TLS support in the connection pooler requires
StreamInner to handle encrypted TCP connections alongside plain TCP and Unix sockets.

What changed: StreamInner now has a TCPTls variant wrapping TlsStream<TcpStream>.
create_tcp_stream_inner accepts ServerTlsConfig instead of bare booleans and performs
SSLRequest/handshake negotiation based on the configured TLS mode — connecting with
TLS when the server supports it, falling back to plain TCP when the mode allows it,
or failing when TLS is required but unavailable.
What was needed: cancel requests to PostgreSQL must use TLS when the main pooled connection was established over TLS.
Essence: Server::startup() now uses per-address TLS config, ClientServerMap stores TLS info alongside connection details, and cancel requests reuse the same TLS configuration as the original connection.
The old server_tls (bool) and verify_server_certificate (bool) fields were
replaced with server_tls_mode, server_tls_ca_cert, server_tls_certificate,
and server_tls_private_key, but fields.yaml and the reference configs were
not updated — breaking 9 tests in src/app/generate/.

Updated fields.yaml for both general and pool sections and regenerated
pg_doorman.toml and pg_doorman.yaml from the new field definitions.
…e, per-pool)

New step "PostgreSQL SSL certificates are generated" creates a full PKI
(CA + server cert with SAN + client cert + wrong CA) so feature scenarios
can start PostgreSQL with SSL enabled and point pg_doorman at the certs
via placeholder substitution.

Placeholder substitution is now also applied to the PostgreSQL options
string, allowing ${PG_SSL_CERT} / ${PG_SSL_KEY} inside pg_ctl -o flags.
Comment thread src/config/mod.rs
info!("Server TLS CA cert: {ca}");
}
if let Some(ref cert) = self.general.server_tls_certificate {
info!("Server TLS certificate: {cert}");
Comment thread src/config/tls.rs

match mode {
ServerTlsMode::Prefer | ServerTlsMode::Require => {
builder.danger_accept_invalid_certs(true);
Comment thread src/config/tls.rs
match mode {
ServerTlsMode::Prefer | ServerTlsMode::Require => {
builder.danger_accept_invalid_certs(true);
builder.danger_accept_invalid_hostnames(true);
Comment thread src/config/tls.rs
let ca = load_certificate(ca_path)?;
builder.add_root_certificate(ca);
}
builder.danger_accept_invalid_hostnames(true);
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.

2 participants