Browse docs
Security
Client certificate (mTLS) authentication
Require every connection to a PostgreSQL database to present a valid client certificate in addition to its password. Each database gets its own certificate authority, certificates are issued and revoked from the dashboard or API, and enforcement happens inside Postgres itself, so the database verifies who is connecting, not just what password they hold.
Overview
How it works
Client certificate authentication is a Pro feature, available on PostgreSQL cloud databases, opt-in per database and fully reversible. When you enable it, every external connection must complete a mutual TLS handshake with a certificate signed by that database's certificate authority, and then still authenticate with the usual SCRAM password. The certificate and the password are both required. It is never certificate-only.
- Per-database CA. Each database has its own certificate authority. A certificate issued for one database is worthless against any other.
- Certificate and password. Enforcement stacks on top of SCRAM (
clientcert=verify-ca), so a leaked password alone cannot connect and a certificate alone cannot either. - TLS terminates at your database. Layerbase pipes the raw TLS stream straight to your Postgres container, so the handshake, and the certificate check, happen against your database rather than a shared proxy fleet. That is what makes engine-native client certificates possible here.
Setup
Issue a certificate, then enable
Enabling enforcement requires at least one active certificate to exist first, so you can never lock yourself out. The flow is two steps, both from the database's Security panel or the API.
- 1. Issue a certificate. The database creates its CA on the first issue and hands you a one-time bundle: the client certificate, its private key, and the CA certificate. The private key is generated in the container, returned once, and stored nowhere. Download all three files immediately, because the key is shown only this once. The CA certificate stays re-downloadable later; the key does not.
- 2. Enable enforcement. With at least one certificate issued, turn on enforcement. From that point every new connection needs both a certificate and the password. If the database is hibernated, enforcement is applied on its next wake.
Store the three files somewhere your client can read them, and lock down the private key so only your user can read it:
# The dashboard downloads three files: client.crt, client.key, ca.crt
$ chmod 0600 client.key
Connect
Connect with the certificate
Point your client at your client.crt and client.key. The examples below also verify the server (sslmode=verify-full), so trust is mutual in both directions. Our Postgres server presents a public Let's Encrypt certificate, so your client validates it against the operating system trust store (sslrootcert=system), not against your per-database CA.
Do not pass ca.crt as sslrootcert. The ca.crt in your bundle is the CA that signed your client certificate. The server uses it to verify you; it does not sign our server certificate, so pointing sslrootcert at it fails with certificate verify failed. Your client verifies our Let's Encrypt server certificate using the system trust store (sslmode=verify-full sslrootcert=system). Keep ca.crt for your records; it is not a connection input.
$ psql "sslmode=verify-full \
sslrootcert=system \
sslcert=client.crt \
sslkey=client.key \
host=your-db.cloud.layerbase.com port=5432 \
dbname=app user=app_user"
sslrootcert=system needs libpq 16 or newer; on an older client, point sslrootcert at your OS CA bundle path instead (for example /etc/ssl/certs/ca-certificates.crt), never at ca.crt. psql also refuses a private key that is group or world readable, so keep client.key at chmod 0600. You will still be prompted for (or supply) the role's password.
import { readFileSync } from 'node:fs'
import { Client } from 'pg'
// Present your client cert + key. Do NOT set ssl.ca to your per-database
// ca.crt: that is the CA for your client cert, not for our server. Our
// Postgres serves a public Let's Encrypt cert, so with ssl.ca omitted Node
// verifies the server against its bundled roots and rejectUnauthorized: true.
const client = new Client({
host: 'your-db.cloud.layerbase.com',
port: 5432,
database: 'app',
user: 'app_user',
password: process.env.PGPASSWORD,
ssl: {
cert: readFileSync('client.crt', 'utf8'),
key: readFileSync('client.key', 'utf8'),
rejectUnauthorized: true,
},
})
await client.connect()// Use postgres.js (npm:postgres): its ssl option takes standard
// tls.connect fields incl. client certs. The deno.land/x postgres
// driver only supports caCertificates and cannot do mTLS.
import postgres from 'npm:postgres'
// Store your client cert + key as function secrets, not files:
// supabase secrets set DB_CERT="$(cat client.crt)" \
// DB_KEY="$(cat client.key)"
// Do NOT set ssl.ca to your per-database ca.crt. Our Postgres serves a
// public Let's Encrypt cert, so with ssl.ca omitted the runtime verifies
// the server against its bundled roots and rejectUnauthorized: true.
const sql = postgres({
host: 'your-db.cloud.layerbase.com',
port: 5432,
database: 'app',
username: 'app_user',
password: Deno.env.get('DB_PASSWORD'),
ssl: {
cert: Deno.env.get('DB_CERT'),
key: Deno.env.get('DB_KEY'),
rejectUnauthorized: true,
},
})
const [row] = await sql`select 1 as ok`This is the case client certificates were built for: an edge runtime with rotating egress IPs, where IP allowlisting cannot pin a source. The PEMs live as environment secrets, so you get mTLS from a platform that does not itself offer it.
Pooling
Connection pooling stays on
Enforcement happens at the connection pooler itself: PgBouncer verifies the client certificate on every connection before it reaches Postgres. So you keep the pooled endpoint with client certificate auth turned on, which matters for serverless and other high-connection-count workloads. Both the pooled and the direct connection strings work, and both require the certificate. If you also want network-level restrictions, pair this with IP allowlisting.
Rotation
Rotating the CA revokes everything
There is no per-certificate revocation list in v1. To revoke a leaked certificate you rotate the database's certificate authority, which generates a fresh CA and immediately invalidates every certificate ever issued under the old one. Rotation hands you a new one-time bundle to redistribute. Plan for a brief window where existing clients need their new files before they can reconnect.
One caveat worth knowing: restoring a backup taken before a rotation restores the old CA material along with the data, which resurrects the certificates that rotation revoked. If you rotate to contain a leak and later restore a pre-rotation backup, rotate again afterward so the exposed certificates stay dead.
Troubleshooting
Common errors
- "connection requires a valid client certificate". Enforcement is on but your client did not present a certificate. Pass both
sslcertandsslkey(or the driver equivalents), and confirm you are on the direct connection string, not the pooled one. - "SSL error: certificate verify failed". You almost certainly pointed
sslrootcert(or the driver'scaoption) at your per-databaseca.crt. That CA signs your client certificate, not our server, so it can never verify our Let's Encrypt server certificate. Usesslrootcert=system(or omit the drivercaoption) so the system trust store validates the server. - "private key file has group or world access". Tighten the key's permissions with
chmod 0600 client.key. Most clients refuse a key any other user can read. - Password prompt still appears. That is expected. The certificate does not replace the password; both are required. A wrong password with a valid certificate is still rejected at the SCRAM step.
- A certificate stopped working after a rotation. Rotation revokes every certificate issued under the previous CA. Issue a fresh certificate from the new CA and redistribute the bundle.
Pair it
Combine with IP allowlisting
Client certificates answer "who is connecting"; IP allowlisting answers "from where". They stack cleanly. Where your source IPs are stable, use allowlisting on its own. Where they rotate, or where a network boundary is not enough for your internal requirements, certificates carry identity that follows the client wherever it runs.