Encrypting a logical replication connection means choosing an sslmode strong enough that the decoded row-level change stream — every INSERT, UPDATE, and DELETE on your published tables — cannot be read or tampered with in transit, and this is one of the security boundaries and permissions that a consumer either enforces end-to-end or silently forfeits. Stop at sslmode=require and the traffic is encrypted but a forged server passes authentication; only verify-full closes the man-in-the-middle path that would otherwise expose the entire change feed.
This page covers the transport-crypto layer specifically: the sslmode ladder up to verify-full and what each rung defends against, the server ssl configuration and CA/certificate setup, client-certificate authentication for the replication role, and certificate rotation without dropping the stream. The pg_hba.conf rule that routes a hostssl replication connection is a companion topic covered in setting up pg_hba.conf for replication users; here the focus is the TLS handshake itself. All behavior is validated against PostgreSQL 14 through 17.
sslmode value is a security level, not a toggle: require merely encrypts, while verify-full is the only rung that authenticates the server and blocks an active interception of the change stream.sslmode Protection Semantics
sslmode is a client-side setting that decides how far libpq will go to protect and authenticate the connection, and for a replication consumer the gap between “encrypted” and “authenticated” is the whole attack surface. The table fixes what each value guarantees and the threat it leaves open, targeting PostgreSQL 14–17.
sslmode |
Encryption | Server authentication | Threat left open |
|---|---|---|---|
disable |
None | None | Everything — change stream in cleartext |
allow |
Only if the server demands it | None | Eavesdropping; encryption is not guaranteed |
prefer (libpq default) |
Opportunistic | None | Eavesdropping — silently falls back to plaintext |
require |
Always | None | Active man-in-the-middle; any server cert is accepted |
verify-ca |
Always | Certificate chains to a trusted CA (sslrootcert) |
Hostname spoofing — a valid cert for any host passes |
verify-full |
Always | Cert chains to the CA and matches the server hostname | None of the above — the correct replication default |
The operationally important line is require versus verify-full. require gives a false sense of safety: the connection is encrypted, but because no certificate is validated, an attacker who can redirect the connection presents their own certificate and terminates the TLS session themselves, reading and modifying every decoded change before forwarding it. verify-ca closes the forged-certificate hole but still accepts a valid certificate issued for a different host, so a compromised but legitimately-signed server elsewhere in your estate can impersonate the publisher. Only verify-full, with sslrootcert pointing at your CA, authenticates that you are talking to the intended primary. Treat any replication DSN below verify-full as a finding. These modes pair with the role and network controls in the parent security boundaries and permissions guide.
Diagnostic Patterns
Prove that live replication connections are actually encrypted — a hostssl rule guarantees the server requires TLS, but you still want to confirm the negotiated protocol and cipher. pg_stat_ssl joined to pg_stat_replication shows exactly what each streaming consumer negotiated:
-- Encryption state of every connected replication consumer.
SELECT r.application_name, r.client_addr, r.state,
s.ssl, s.version AS tls_version, s.cipher,
s.client_dn -- non-null only with client-cert auth
FROM pg_stat_replication r
JOIN pg_stat_ssl s ON s.pid = r.pid
ORDER BY r.application_name;
Any row with ssl = false is a plaintext replication stream and an immediate finding; a tls_version below TLSv1.2 means an outdated client negotiated a weak protocol and ssl_min_protocol_version should be raised. Confirm the server is presenting the certificate you expect and that it has not silently expired — an expired server certificate rejects every new verify-* connection while leaving established ones alive, so the breakage looks intermittent:
# Inspect the server cert the publisher actually presents, including expiry.
openssl s_client -connect publisher.internal:5432 -starttls postgres </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
Check the server-side TLS configuration is loaded and enforces a modern floor:
SELECT name, setting FROM pg_settings
WHERE name IN ('ssl','ssl_cert_file','ssl_ca_file','ssl_min_protocol_version');
A require/verify-* client against a server showing ssl = off fails the handshake entirely; that mismatch is the most common reason a consumer that authenticated yesterday cannot connect after a config change.
Safe Deployment Sequence
Enabling TLS on the server is a reload for most settings but the initial ssl = on may require a restart depending on platform, so stage it as a controlled change with a tested revert.
-
Install the server key and certificate with strict permissions — PostgreSQL refuses to start if the private key is group- or world-readable:
bash install -o postgres -g postgres -m 0600 server.key /etc/postgresql/ssl/server.key install -o postgres -g postgres -m 0644 server.crt /etc/postgresql/ssl/server.crt install -o postgres -g postgres -m 0644 ca.crt /etc/postgresql/ssl/ca.crt -
Enable TLS and pin the protocol floor in the server configuration:
sql ALTER SYSTEM SET ssl = on; ALTER SYSTEM SET ssl_cert_file = '/etc/postgresql/ssl/server.crt'; ALTER SYSTEM SET ssl_key_file = '/etc/postgresql/ssl/server.key'; ALTER SYSTEM SET ssl_ca_file = '/etc/postgresql/ssl/ca.crt'; -- enables client-cert verification ALTER SYSTEM SET ssl_min_protocol_version = 'TLSv1.2'; SELECT pg_reload_conf(); -- restart only if ssl was off at boot -
Require client certificates for the replication role by adding
clientcert=verify-fullto thehostsslline, so a stolen password alone cannot open the stream — the rule syntax is detailed in setting up pg_hba.conf for replication users:code # TYPE DATABASE USER ADDRESS METHOD OPTIONS hostssl replication repl_etl_svc 10.0.5.0/24 scram-sha-256 clientcert=verify-full -
Prove
verify-fullend-to-end from a consumer host before cutting over, using the client key and the pinned CA:bash psql "host=publisher.internal dbname=replication user=repl_etl_svc \ sslmode=verify-full sslrootcert=ca.crt sslcert=client.crt sslkey=client.key" \ -c "IDENTIFY_SYSTEM" -
Revert. Because
ssland the file paths are reload-scoped after the first enable, rollback isALTER SYSTEM RESET ssl;pluspg_reload_conf()— but never revert while consumers are pinned toverify-full, or every stream fails the handshake at once. Lower clientsslmodefirst, then disable server TLS.
Pipeline Integration
A Python consumer must pin verify-full with an explicit sslrootcert in its DSN and treat a certificate failure as fatal, non-retryable — silently falling back to plaintext is exactly the failure TLS is meant to prevent. Build the connection string so encryption is not negotiable:
# Replication consumer that refuses anything weaker than verify-full.
import psycopg2
from psycopg2.extras import LogicalReplicationConnection
DSN = (
"host=publisher.internal dbname=replication user=repl_etl_svc "
"sslmode=verify-full " # authenticate the server, not just encrypt
"sslrootcert=/etc/cdc/ca.crt " # pin the CA — the anti-MITM control
"sslcert=/etc/cdc/client.crt " # client-cert auth for the replication role
"sslkey=/etc/cdc/client.key"
)
def open_stream():
try:
return psycopg2.connect(DSN, connection_factory=LogicalReplicationConnection)
except psycopg2.OperationalError as exc:
msg = str(exc)
if "certificate verify failed" in msg or "SSL" in msg:
# A cert/hostname failure is a security event, not a transient blip. Do NOT retry.
raise TlsVerificationError(msg) from exc
raise # network/transient: caller applies backoff
Two rules keep TLS from becoming the thing that breaks the pipeline. First, rotate certificates with overlap, never with a hard swap: issue the new server and client certificates from the same CA before the old ones expire, distribute the new client key alongside the old, and reload the server (pg_reload_conf() rereads ssl_cert_file without dropping live connections) so there is always a valid pair on both ends during the window. Second, monitor expiry as a metric, exporting days-to-expiry for the server and client certificates so a rotation is scheduled, not triggered by an outage — an expired certificate rejects new verify-full connections while existing streams keep running, producing exactly the intermittent failure that is hardest to diagnose. Feed those gauges into the same collector used for slot health so certificate and stream alerts arrive together.
Failover handling. A promoted standby must present a certificate the consumers already trust — either a certificate whose subject alternative names include every publisher hostname in the topology, or per-node certificates all issued by the pinned CA. A divergent or missing certificate on the standby is a classic reason a verify-full consumer cannot reconnect after failover, since the hostname or chain no longer matches. Provision the standby’s TLS material and ssl configuration identically to the primary, and confirm the slot the consumer reattaches to is intact as covered in initializing replication slots.
Authoritative references
- PostgreSQL: SSL Support (
sslmode, client certificates) — the fullsslmodeladder andsslrootcert/sslcert/sslkeysemantics. - PostgreSQL: Secure TCP/IP Connections with SSL (
ssl,ssl_cert_file) — server-side certificate and key configuration. - PostgreSQL:
pg_stat_ssl— the view that reports negotiated protocol, cipher, and client certificate. - PostgreSQL: Authentication Methods — Certificate Authentication —
clientcert=verify-fulland thecertmethod.
Related
- Setting up pg_hba.conf for replication users — the
hostsslrule that routes the encrypted replication connection this page secures. - Security boundaries and permissions — the parent guide to roles, grants, and network isolation TLS sits within.
- Initializing replication slots — provisioning the slot a TLS-authenticated consumer reattaches to after failover.