By default, self-hosted Supabase allows unencrypted database connections. Your Docker compose setup works, your application connects, data flows—but every query, every authentication token, every piece of user data travels in plain text. If anyone on the network path can intercept traffic (and in cloud environments, that's more people than you think), they can read everything.
This isn't theoretical. Database credential theft remains one of the most common attack vectors for data breaches. Encrypting connections with SSL/TLS is table stakes for production-ready self-hosted Supabase. Yet it's often skipped because the default configuration "just works" without it.
Let's fix that. This guide walks through enabling SSL encryption for PostgreSQL connections in your self-hosted Supabase instance, from generating certificates to enforcing encrypted connections for all external clients.
Why SSL Matters for Database Connections
SSL/TLS encryption serves three purposes for database connections:
Confidentiality: Data transmitted between your application and database is encrypted. Attackers who intercept traffic see gibberish instead of SQL queries containing user credentials, payment details, or session tokens.
Integrity: SSL prevents man-in-the-middle attacks where an attacker modifies queries or responses in transit. You know the data you sent is the data the database received.
Authentication: With proper certificate verification, clients can confirm they're connecting to the legitimate database server, not an imposter intercepting connections.
Supabase Cloud enforces SSL by default and provides downloadable CA certificates for verify-full connections. Self-hosted deployments need manual configuration, but the good news is that PostgreSQL has excellent SSL support built in.
Understanding SSL Modes
PostgreSQL supports several SSL connection modes, each offering different security guarantees:
| Mode | Encryption | Server Verification | Use Case |
|---|---|---|---|
disable | No | No | Never use in production |
allow | Optional | No | Legacy compatibility only |
prefer | If available | No | Better than nothing |
require | Yes | No | Encrypted but vulnerable to MITM |
verify-ca | Yes | CA certificate | Validates server cert chain |
verify-full | Yes | CA + hostname | Maximum security |
For production self-hosted Supabase, you should target verify-full mode. This ensures connections are encrypted AND clients verify they're talking to the correct server.
Prerequisites
Before configuring SSL, ensure your self-hosted Supabase deployment is working correctly without SSL. Don't apply these changes to a fresh deployment—the initialization scripts expect unencrypted internal connections initially.
You'll need:
- A running self-hosted Supabase instance
- Root/sudo access to your server
- OpenSSL installed (standard on most Linux distributions)
- Basic familiarity with Docker Compose
Generating SSL Certificates
PostgreSQL requires three files for SSL:
- A Certificate Authority (CA) certificate
- A server certificate signed by that CA
- The server's private key
For self-hosted deployments, self-signed certificates work fine for encryption. You only need certificates from a public CA if clients need to verify the server's identity against a publicly trusted root.
Create a directory for your certificates:
mkdir -p /opt/supabase/ssl cd /opt/supabase/ssl
Generate the CA Certificate
First, create a private key and certificate for your CA:
# Generate CA private key openssl genrsa -out ca.key 4096 # Create CA certificate (valid for 10 years) openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \ -out ca.crt \ -subj "/CN=Supabase-Self-Hosted-CA"
Generate the Server Certificate
Next, create the server certificate that PostgreSQL will use:
# Generate server private key openssl genrsa -out server.key 4096 # Set restrictive permissions (PostgreSQL requires this) chmod 600 server.key # Create certificate signing request openssl req -new -key server.key \ -out server.csr \ -subj "/CN=postgres" # Sign the server certificate with your CA openssl x509 -req -in server.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt -days 3650 -sha256
Your /opt/supabase/ssl directory should now contain:
ca.crt # CA certificate (distribute to clients) ca.key # CA private key (keep secure) server.crt # Server certificate server.key # Server private key (keep secure) server.csr # Certificate request (can delete)
Configuring PostgreSQL for SSL
Now you need to configure PostgreSQL to use these certificates. The approach varies depending on your deployment method.
Docker Compose Configuration
For standard Docker Compose deployments, modify your PostgreSQL service to mount the certificates and enable SSL.
Edit your docker-compose.yml:
services:
db:
image: supabase/postgres:15.1.1.78
# ... existing configuration ...
volumes:
- ./volumes/db/data:/var/lib/postgresql/data:Z
- /opt/supabase/ssl/server.crt:/var/lib/postgresql/server.crt:ro
- /opt/supabase/ssl/server.key:/var/lib/postgresql/server.key:ro
- /opt/supabase/ssl/ca.crt:/var/lib/postgresql/ca.crt:ro
command:
- postgres
- -c
- ssl=on
- -c
- ssl_cert_file=/var/lib/postgresql/server.crt
- -c
- ssl_key_file=/var/lib/postgresql/server.key
- -c
- ssl_ca_file=/var/lib/postgresql/ca.crt
The key additions:
- Volume mounts for the SSL certificates (read-only)
- Command-line arguments enabling SSL and pointing to certificate locations
Kubernetes/Helm Configuration
For Kubernetes deployments, you'll use a Secret and ConfigMap approach. Create a secret for the certificates:
kubectl create secret generic supabase-postgres-ssl \ --from-file=server.crt=/opt/supabase/ssl/server.crt \ --from-file=server.key=/opt/supabase/ssl/server.key \ --from-file=ca.crt=/opt/supabase/ssl/ca.crt
Then update your Helm values to mount the secret and configure PostgreSQL. Check our Kubernetes deployment guide for detailed Helm configuration.
Enforcing SSL Connections
Enabling SSL isn't enough—you need to enforce it. Otherwise, clients can still connect without encryption.
Modify pg_hba.conf
PostgreSQL's host-based authentication file controls connection requirements. The default pg_hba.conf allows unencrypted connections. You need to change it to require SSL for external connections.
Create a custom pg_hba.conf:
# TYPE DATABASE USER ADDRESS METHOD # Allow password-less connections from localhost (internal services) local all all trust # Internal Docker network - require password, SSL optional # (Internal services like PostgREST connect here) host all all 172.16.0.0/12 scram-sha-256 # External connections - require SSL hostssl all all 0.0.0.0/0 scram-sha-256
The critical line is hostssl instead of host for external connections. This tells PostgreSQL to reject any non-SSL connection attempts from outside the internal network.
Mount this configuration in your Docker Compose:
services:
db:
# ... existing configuration ...
volumes:
- ./pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
command:
- postgres
- -c
- ssl=on
- -c
- ssl_cert_file=/var/lib/postgresql/server.crt
- -c
- ssl_key_file=/var/lib/postgresql/server.key
- -c
- ssl_ca_file=/var/lib/postgresql/ca.crt
- -c
- hba_file=/etc/postgresql/pg_hba.conf
Restart PostgreSQL
Apply the changes by restarting the database container:
docker compose restart db
Monitor the logs to ensure PostgreSQL starts correctly with SSL enabled:
docker compose logs db | grep -i ssl
You should see messages like:
LOG: SSL: certificate file "/var/lib/postgresql/server.crt" loaded LOG: SSL: key file "/var/lib/postgresql/server.key" loaded LOG: listening on IPv4 address "0.0.0.0", port 5432
Testing SSL Connections
Verify that SSL is working correctly by testing connections with different modes.
Test with psql
If you have psql installed locally, test the connection:
# This should work (SSL required, encryption enabled) psql "postgresql://postgres:your_password@your_server:5432/postgres?sslmode=require" # This should also work with certificate verification psql "postgresql://postgres:your_password@your_server:5432/postgres?sslmode=verify-ca&sslrootcert=/path/to/ca.crt" # This should FAIL (SSL disabled) psql "postgresql://postgres:your_password@your_server:5432/postgres?sslmode=disable"
If sslmode=disable still connects, your pg_hba.conf isn't properly enforcing SSL.
Verify Encryption in psql
Once connected, verify the connection is encrypted:
SELECT ssl, version FROM pg_stat_ssl WHERE pid = pg_backend_pid();
Output should show:
ssl | version -----+--------- t | TLSv1.3
Configuring Application Connections
Your applications need to use SSL when connecting to the database. Here's how to configure common Supabase client libraries.
Node.js/JavaScript
import { createClient } from '@supabase/supabase-js'
// For API access (goes through Kong/PostgREST, already SSL)
const supabase = createClient(
'https://your-supabase-domain.com',
'your-anon-key'
)
// For direct database connections (if using postgres.js or similar)
import postgres from 'postgres'
const sql = postgres({
host: 'your-server.com',
port: 5432,
database: 'postgres',
username: 'postgres',
password: 'your_password',
ssl: {
rejectUnauthorized: true,
ca: fs.readFileSync('/path/to/ca.crt').toString()
}
})
Python
import psycopg2
conn = psycopg2.connect(
host="your-server.com",
port=5432,
database="postgres",
user="postgres",
password="your_password",
sslmode="verify-full",
sslrootcert="/path/to/ca.crt"
)
Connection Pooling Considerations
If you're using connection pooling with PgBouncer or Supavisor, SSL configuration depends on your setup:
- Client to pooler: Configure SSL on your reverse proxy (Nginx/Caddy)
- Pooler to PostgreSQL: Configure SSL on the internal connection
For Supavisor specifically, set the environment variables:
DATABASE_URL=postgresql://postgres:password@db:5432/postgres?sslmode=require
Distributing CA Certificates
Clients using verify-ca or verify-full modes need your CA certificate. You have several options for distribution:
Manual distribution: Provide ca.crt to developers and include it in deployment artifacts.
Environment variable: Encode the certificate and pass it as an environment variable:
export SUPABASE_CA_CERT=$(base64 -w0 /opt/supabase/ssl/ca.crt)
Documentation: Host the CA certificate at a well-known URL in your infrastructure docs.
Monitoring SSL Connections
Track SSL usage to ensure all connections are encrypted. Query pg_stat_ssl to see connection details:
SELECT usename, datname, ssl, version, cipher, client_addr FROM pg_stat_activity a JOIN pg_stat_ssl s ON a.pid = s.pid WHERE state = 'active';
Add this to your monitoring setup to alert on any non-SSL connections (which shouldn't exist if enforcement is working).
Troubleshooting Common Issues
"SSL SYSCALL error: Connection reset by peer"
This usually means certificate permissions are wrong. PostgreSQL requires the private key to be readable only by the postgres user:
chmod 600 /opt/supabase/ssl/server.key chown 999:999 /opt/supabase/ssl/server.key # UID 999 is postgres in the container
"certificate verify failed"
The client can't verify the server certificate. Ensure:
- You're using the correct CA certificate
- The certificate hasn't expired
- Hostname matches if using
verify-full
Internal Services Failing After SSL Enforcement
If PostgREST, Auth, or other Supabase services can't connect after enabling SSL enforcement, check your pg_hba.conf. Internal Docker network connections should not require SSL:
host all all 172.16.0.0/12 scram-sha-256
Certificate Renewal
Self-signed certificates with 10-year validity don't need frequent renewal, but you should plan for it:
- Generate new certificates before expiration
- Update volume mounts or secrets
- Restart PostgreSQL during a maintenance window
- Distribute new CA certificate to clients (if changed)
Consider automating this process or setting calendar reminders well before expiration.
Performance Considerations
SSL adds computational overhead to connections, but modern CPUs handle it efficiently:
- Connection overhead: Initial TLS handshake adds ~2-5ms latency
- Throughput impact: Negligible with modern hardware and TLS 1.3
- CPU usage: Minimal increase (<5% typically)
Connection pooling helps amortize handshake costs across many queries. If you're seeing significant SSL overhead, it's usually a sign you need connection pooling, not that you should disable SSL.
Next Steps
With SSL encryption enabled, your self-hosted Supabase database connections are protected in transit. Consider these additional security measures:
- Network security: Restrict database port access with firewall rules
- API key rotation: Regularly rotate your Supabase API keys
- Backup encryption: Ensure your backups are encrypted at rest
