Database SSL mismatch
Apps can't connect to Postgres with TLS handshake errors
Kratos, Hydra, Athena, Hera, or Site can't connect to Postgres. Logs show:
TLS handshake failed: x509: certificate signed by unknown authorityor:
SSL connection failed: server certificate verification failedor:
SSL is required by server but client requests sslmode=disableThese all indicate a mismatch between what the client expects and what the server provides.
Diagnostic
Connection string
ssh prod 'podman exec ciam-kratos sh -c "echo \$DSN"'Look for sslmode= in the URL. Production Olympus requires sslmode=verify-full (ADR 0013).
CA bundle inside the container
ssh prod 'podman exec ciam-kratos ls -la /etc/ssl/certs/'The CA cert that signed your Postgres server cert must be reachable here. The path depends on the connection string:
sslrootcert=/etc/ssl/certs/postgres-ca.crt, Kratos looks here.
CA cert validity
ssh prod 'podman exec ciam-kratos openssl x509 -in /etc/ssl/certs/postgres-ca.crt -noout -dates'If the CA cert has expired, every TLS handshake fails. Generate a new CA, see Operate, Cert Rotation.
Server hostname vs cert
echo | openssl s_client -connect $POSTGRES_HOST:5432 -starttls postgres 2>/dev/null \
| openssl x509 -noout -text | grep -A1 "Subject Alternative Name"verify-full checks the cert's SAN against the hostname in DATABASE_URL. Mismatch = handshake fail.
If your DATABASE_URL uses db.internal.example.com but the cert was issued for postgres.example.com, change one to match.
Fixes
CA cert is wrong / missing
Re-distribute. The CA cert should be baked into each app image at build time or mounted from a known location:
volumes:
- /etc/ssl/certs/postgres-ca.crt:/etc/ssl/certs/postgres-ca.crt:roFor Neon-managed Postgres, the CA is published, fetch and bundle:
curl https://neon.tech/ca.crt > postgres-ca.crtCert is expired
Generate new CA + server cert, deploy to Postgres, redeploy apps with the new CA bundle. See Operate, Cert Rotation.
sslmode=verify-full vs server cert hostname mismatch
Either:
- Regenerate the server cert with a SAN matching the hostname your apps use.
- Or change the hostname in
DATABASE_URLto match the cert's SAN.
The first is correct; the second is a workaround.
Server requires SSL but client uses disable
Production should never use sslmode=disable. Update DATABASE_URL to sslmode=verify-full and ensure CA is in place.
The verify-prod-config.yml CI workflow catches this for production deployments, if it slipped through, your deploy bypassed CI.