Database SSL Encryption for Self-Hosted Supabase: A Complete Security Guide

Learn how to enable and enforce SSL/TLS encryption for PostgreSQL connections in self-hosted Supabase to protect data in transit.

Cover Image for Database SSL Encryption for Self-Hosted Supabase: A Complete Security Guide

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:

ModeEncryptionServer VerificationUse Case
disableNoNoNever use in production
allowOptionalNoLegacy compatibility only
preferIf availableNoBetter than nothing
requireYesNoEncrypted but vulnerable to MITM
verify-caYesCA certificateValidates server cert chain
verify-fullYesCA + hostnameMaximum 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:

  1. Generate new certificates before expiration
  2. Update volume mounts or secrets
  3. Restart PostgreSQL during a maintenance window
  4. 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:

Further Reading