Guide
PostgreSQL SSL certificate expired
PostgreSQL rejects or aborts SSL connections when server.crt is past notAfter. Test pg_hba TLS with openssl s_client and reload postgresql after installing a renewed chain.
Updated May 9, 2026 803 words
SSL error: certificate verify failedfrompsqlor libpq usually means PostgreSQL presented an expiredserver.crt, not that your connection string typo broke DNS.
Platform engineers chasing postgresql ssl certificate expired typically enforce sslmode=verify-full in ORMs while the database still serves a leaf whose notAfter passed last weekend. Because PostgreSQL speaks TLS immediately on port 5432 (when SSL is enabled), you can fingerprint expiry without guessing which HTTP reverse proxy might share the hostname.
RDS, Cloud SQL, and Crunchy Bridge rotate provider-managed server identities on their own schedule; self-managed clusters on Kubernetes or bare metal depend on files referenced in postgresql.conf and timely pg_ctl reload. Treat connection pooler certificates (PgBouncer, Odyssey) as a second listener to test.
Vacuum, autovacuum, and logical replication workers are separate OS processes but reuse the same server listener certificate—when it expires, admin psql sessions and application pools fail together. Performance insights dashboards may look nominal until connection attempts spike; correlate connection error metrics with openssl rather than assuming query regressions.
Operators running certificate rotation tutorials sometimes rotate only the client CA while leaving server.crt expired—double-check which file your incident references before closing. Extensions such as pgaudit or pg_stat_statements do not alter TLS behavior but generate noise in logs during mass reconnect storms—filter for SSL keywords first.
Connection string secrets in Kubernetes may reference pooler DNS while break-glass admins use primary DNS—renewal validation must openssl every name in git, not only the one the incident reporter used.
Failover drills should include verify-full clients so standby promotion never surprises you with an expired cert on the hidden replica URL. Document replica DSN hostnames in the same runbook as the primary.
First response
Log the client message (SSL certificate has expired, certificate verify failed, JDBC PSQLException), the target host from PGHOST / connection URL, and sslmode. Capture SELECT version(); only if you can already connect—otherwise start with wire proof.
From outside the app network, handshake to host:5432 and save certificate dates. On the server, record SHOW ssl; and the paths behind ssl_cert_file, ssl_key_file, and ssl_ca_file. If PgBouncer sits in front, test both the pooler port and the upstream Postgres port separately.
Link related database incidents to MySQL SSL certificate expired when polyglot stacks fail the same night.
Include pg_stat_ssl output when connections already work from a break-glass account: compare client versus server flags, cipher, and whether the backend cert subject appears in the result. If only some roles fail, check for older ODBC drivers on Windows Citrix hosts that pin legacy TLS versions—expiry remains the root cause but error strings vary. Export CloudWatch or Azure diagnostic logs for flexible server instances when openssl and vendor portals disagree.
What this error means
With ssl = on, Postgres offers TLS before the startup message completes authentication. Verifying clients check the chain, trust, and validity window. Expired server material fails before pg_hba.conf rules matter for most drivers.
sslmode=require encrypts but may skip verification—teams discover expiry only after tightening to verify-ca or verify-full. Client certificates for mutual TLS (sslmode=verify-full with client certs) are independent; do not confuse them with the server leaf expiry.
Patroni, repmgr, or streaming replication do not copy renewed PEMs automatically—each node needs the same rotation discipline.
Application teams sometimes misread no pg_hba.conf entry as auth failure when the connection never reached pg_hba because TLS verification aborted first—grep logs for SSL before tuning roles. Cloud-native offerings may rotate without your action yet still emit maintenance notices; openssl verifies what your app experiences regardless of ticket semantics.
Common causes
Certbot renewed files under /etc/letsencrypt/live/ but ssl_cert_file still points at an old symlink target copied months ago. Ansible copied server.crt while postgresql was running without pg_ctl reload, leaving workers serving stale chains until restart.
Helm updated a Secret volume but the Postgres pod was not recreated. A wildcard cert expired while a SAN-specific cert still covered analytics—verify-full against the warehouse hostname hits the wrong listener. Clock skew on VMs is rare but can surface as certificate not yet valid instead of expired; still verify notAfter on the wire first.
Supabase, Neon, and other hosted Postgres products manage server TLS for you—openssl to their documented hostnames still proves what clients see when disputes arise. Citus coordinators and workers expose different ports; pgbouncer transaction pooling can hide server settings while apps pin to the pooler hostname. Logical replication subscribers are not exempt: logical decoding connections with sslmode=verify-full fail when the publisher leaf expires even while physical replication lag metrics look nominal.
Confirm from the outside
Start from the hostname in application secrets, not the friendly alias in dashboards. If multiple regions exist, run the same openssl command in each region’s connectivity path or from CI agents pinned to that geography. Save outputs in the ticket because Postgres incidents often reopen when one replica was missed during renewal.
openssl s_client -connect db.example.com:5432 -starttls postgres </dev/null 2>/dev/null | openssl x509 -noout -dates -subject -issuer
Strict client reproduction:
psql "host=db.example.com port=5432 dbname=app sslmode=verify-full" -c 'select 1'
On the primary (if you have shell):
sudo -u postgres psql -c "SHOW ssl_cert_file; SHOW ssl_ca_file;"
openssl x509 -in "$(sudo -u postgres psql -tAc 'SHOW ssl_cert_file')" -noout -dates
Behind PgBouncer with TLS:
openssl s_client -connect pooler.example.com:6432 </dev/null 2>/dev/null | openssl x509 -noout -dates
ORM layers (psycopg, asyncpg, SQLAlchemy, Hibernate) bubble up SSL SYSCALL error or certificate verify failed depending on libpq build flags. Capture one failing integration test stack trace and align it with openssl against the same DSN host. RDS certificate rotation requires downloading the correct rds-ca-rsa2048-g1 bundle before blaming application regressions when Amazon rotates CAs on a schedule unrelated to your leaf.
How to read the output
Match notAfter between OpenSSL and the on-disk server.crt. Subject and SAN must include the hostname used in verify-full. Issuer changes after PKI migration require updating ssl_ca_file or client trust bundles simultaneously.
Fix checklist
- Map every hostname: primary, replicas, poolers, and logical routes.
- Archive expired serials from external
openssl s_clientoutput. - Issue renewed server cert with correct SANs and key usage for TLS server auth.
- Install
server.crt,server.key, and chain; restrict key permissions (0600, ownerpostgres). - Run
pg_ctl reloadon each instance (or rolling restart if reload insufficient). - Re-run
psqlwith productionsslmode. - Confirm replicas and standbys after
pg_hbaSSL modes still connect. - Document which automation owns files versus RDS parameter groups.
Schedule maintenance so pg_ctl reload runs on primaries and standbys during staffed windows; verify streaming replication reconnects with verify mode enabled. If you temporarily relaxed pg_hba.conf hostssl rules, restore them and capture evidence. Update internal wiki diagrams showing whether TLS ends at PgBouncer or Postgres so the next renewal hits the correct file paths.
Capture pg_stat_ssl summaries from replicas after renewal to confirm client-facing backends negotiated fresh sessions. When using AWS RDS Proxy, renew and validate the proxy endpoint separately from the database instance ARN your DBAs track in the console.
Keep a laminated quick-reference for on-call with openssl s_client -starttls postgres and your top three DSN hostnames—incidents at 03:00 go faster when commands are not googled.
When to use automation
HTTP monitoring of your API does not prove Postgres listener health. Schedule live TLS checks on port 5432 (and pooler ports) using the same DNS names applications resolve. /check helps when the hostname is internet-reachable; private databases need synthetics from inside the VPC.
Pair notAfter alerts with RDAP on domains tied to validation. Threshold alerts at 30, 14, 7, and 1 days give time for replication-aware maintenance. Paid plans add Slack and Discord webhooks for on-call routing.
Database infrastructure teams should tag every Terraform connection_url with the monitored hostname so certificate watches match ORM configs after refactors. When proxies expose multiple ports, create one monitor per port—poolers and primaries diverge often enough to justify two alerts. Include bastion-reachable private names in internal synthetic jobs if public /check cannot see them.
Synthetic SELECT 1 jobs without sslmode alignment create blind spots: pair them with /check on publicly resolvable database proxy hostnames and with nmap SSL cert expiry scans your security team may already run against approved port lists. Document overlap with MySQL SSL certificate expired incidents when polyglot services share one forgotten internal CA.
Post-mortems should ask whether Patroni leader elections masked a stale cert on replicas until promotion moved workloads onto the bad node. Monitoring every hostname in connectionStrings prevents that surprise promotion failure.
Kubernetes operators should verify shared_preload_libraries and cert mounts survived the last Helm upgrade—silent drift in subcharts is common. Legal holds on VM snapshots sometimes restore old server.crt files during DR tests, reintroducing expired serials your monitoring thought were gone; after any restore, rerun external openssl before declaring success.
BI and ETL jobs using ODBC with SSLmode=require may keep running while newer microservices with verify-full fail first—treat that pattern as a canary rather than ignoring the incident. Document every hostname in Dagster, Airflow, and dbt profiles so renewals cover analytics paths, not only OLTP.
Change windows should explicitly list every maintenance consumer: backup agents (pg_basebackup with verify), logical subscribers, and third-party observability exporters using JDBC. Each must reconnect after reload; tail logs for ten minutes after pg_ctl reload to catch latent pool exhaustion. When closing the incident, attach before-and-after openssl serial numbers—not merely “renewed cert” text—so auditors can reconcile CMDB entries.
Schema registry and Debezium connectors often hide Postgres URLs in internal config maps—grep infrastructure repos for sslmode when incidents linger after the primary OLTP URL is fixed.
Frequently asked questions
Can I use `sslmode=require` temporarily?
It encrypts without full verification and is a poor long-term compliance posture. Bridge only while renewing server material.
Managed Postgres still shows expiry in openssl?
Provider certs expire too; AWS documents rds-ca-* rotation. Follow vendor steps rather than dropping local PEMs randomly.
Why did ORM pools fail before kubectl probes?
Probes may use sslmode=disable while apps use verify-full. Align probe TLS with production drivers.
Does pg_repack need TLS reload?
pg_ctl reload is usually enough for cert swap; heavy restarts may be required if your platform caches TLS context differently—retest after reload.
See what the server presents right now
Free live certificate check — no signup.