Skip to main content
The default Supabase Docker configuration is not secure for production. This guide covers essential security measures you must implement before deploying.
Never deploy with default configuration! Default passwords and secrets are publicly known and will be exploited.

Pre-Deployment Checklist

1

Change All Secrets

Replace every default password and secret in .env:
# Generate new secrets
sh ./utils/generate-keys.sh

# Verify no defaults remain
grep -E "your-super-secret|insecure|example" .env
2

Configure HTTPS

Never run production without TLS encryption. Use a reverse proxy with SSL certificates.
3

Enable Firewall

Restrict network access to only required ports.
4

Set Up Backups

Implement automated database backups with off-site storage.
5

Configure SMTP

Set up transactional email for password resets and notifications.

Secrets Management

Critical Secrets

These must be changed before production:
Risk: Root database accessGenerate:
openssl rand -base64 32
Requirements:
  • Minimum 32 characters
  • Mix of letters, numbers, special characters
  • Unique per installation
  • Store in password manager
Risk: Token forgery, authentication bypassGenerate:
openssl rand -base64 64 | tr -d '\n'
Requirements:
  • Minimum 32 characters (64+ recommended)
  • Keep absolutely secret
  • Never expose in client code
  • Rotating requires regenerating all JWT keys
Risk: Administrative access to StudioGenerate:
openssl rand -base64 24
Requirements:
  • Strong password (20+ characters)
  • Enable 2FA if available
  • Rotate quarterly
Risk: Bypasses Row Level SecurityGenerate: Use JWT generator with your JWT_SECRETRequirements:
  • Only use server-side
  • Never expose in frontend code
  • Never commit to repositories
  • Rotate if compromised

Secret Rotation

Regularly rotate sensitive credentials:
# 1. Generate new secrets
NEW_JWT_SECRET=$(openssl rand -base64 64)

# 2. Generate new API keys with new secret
# Visit https://supabase.com/docs/guides/self-hosting/docker#api-keys

# 3. Update .env file
sed -i "s/JWT_SECRET=.*/JWT_SECRET=${NEW_JWT_SECRET}/" .env

# 4. Update applications with new keys

# 5. Restart services
docker compose restart auth rest realtime

Network Security

Firewall Configuration

Allow only necessary ports:
# UFW (Ubuntu/Debian)
sudo ufw default deny incoming
sudo ufw default allow outgoing

# SSH access
sudo ufw allow 22/tcp

# HTTPS for reverse proxy
sudo ufw allow 443/tcp

# Optional: HTTP (redirect to HTTPS)
sudo ufw allow 80/tcp

# Enable firewall
sudo ufw enable
Block direct access to services:
# Only allow localhost access to Kong
sudo ufw deny 8000/tcp

# Block Postgres from external access
sudo ufw deny 5432/tcp

Reverse Proxy with HTTPS

Use nginx or Caddy as a TLS termination proxy:
/etc/nginx/sites-available/supabase
server {
    listen 80;
    server_name api.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.yourdomain.com;
    
    # SSL certificates (use Let's Encrypt)
    ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
    
    # Modern SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    
    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    
    # Proxy to Kong
    location / {
        proxy_pass http://localhost:8000;
        proxy_http_version 1.1;
        
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        proxy_cache_bypass $http_upgrade;
    }
}
Enable and restart:
sudo ln -s /etc/nginx/sites-available/supabase /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

SSL Certificates

Use Let’s Encrypt for free SSL certificates:
# Install Certbot
sudo apt-get update
sudo apt-get install certbot python3-certbot-nginx

# Generate certificate
sudo certbot --nginx -d api.yourdomain.com

# Auto-renewal
sudo certbot renew --dry-run

Database Security

Row Level Security (RLS)

Always enable RLS on user-facing tables:
-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Users can only read their own posts
CREATE POLICY "Users can read own posts"
  ON posts FOR SELECT
  USING (auth.uid() = author_id);

-- Users can only insert their own posts
CREATE POLICY "Users can insert own posts"
  ON posts FOR INSERT
  WITH CHECK (auth.uid() = author_id);

-- Users can only update their own posts
CREATE POLICY "Users can update own posts"
  ON posts FOR UPDATE
  USING (auth.uid() = author_id)
  WITH CHECK (auth.uid() = author_id);

-- Users can only delete their own posts
CREATE POLICY "Users can delete own posts"
  ON posts FOR DELETE
  USING (auth.uid() = author_id);
Tables without RLS policies are accessible to anyone with the service_role key!

Postgres Security

-- Revoke unnecessary permissions
REVOKE ALL ON SCHEMA public FROM PUBLIC;
GRANT USAGE ON SCHEMA public TO anon, authenticated;

-- Limit function execution
REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA public FROM PUBLIC;
GRANT EXECUTE ON FUNCTION your_safe_function TO authenticated;

-- Disable dangerous extensions
DROP EXTENSION IF EXISTS plpythonu;
DROP EXTENSION IF EXISTS plperlu;

-- Enable query logging for auditing
ALTER DATABASE postgres SET log_statement = 'all';
ALTER DATABASE postgres SET log_duration = on;

Connection Security

postgresql.conf
# Require SSL for connections
ssl = on
ssl_cert_file = '/etc/ssl/certs/server.crt'
ssl_key_file = '/etc/ssl/private/server.key'

# Disable weak ciphers
ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL'
ssl_prefer_server_ciphers = on

# Connection limits
max_connections = 100
pg_hba.conf
# Require SSL and password
hostssl all all 0.0.0.0/0 md5
hostssl all all ::/0 md5

# Reject non-SSL
hostnossl all all 0.0.0.0/0 reject

Authentication Security

Password Policies

Enforce strong passwords in GoTrue:
.env
# Minimum password length
GOTRUE_PASSWORD_MIN_LENGTH=12

# Require strong passwords
GOTRUE_PASSWORD_REQUIRED_CHARACTERS=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*

Rate Limiting

Protect against brute force attacks:
volumes/api/kong.yml
plugins:
  - name: rate-limiting
    config:
      minute: 5
      hour: 100
      policy: local

Multi-Factor Authentication

Enable MFA for administrative accounts:
.env
# Enable TOTP MFA
MFA_TOTP_ENROLL_ENABLED=true
MFA_TOTP_VERIFY_ENABLED=true

# Limit MFA factors
MFA_MAX_ENROLLED_FACTORS=10

OAuth Security

Secure OAuth configurations:
# Restrict redirect URLs
ADDITIONAL_REDIRECT_URLS=https://app.yourdomain.com,https://staging.yourdomain.com

# Disable signups if using OAuth only
DISABLE_SIGNUP=true

# Verify email for OAuth users
GOTRUE_EXTERNAL_EMAIL_ENABLED=true
GOTRUE_MAILER_AUTOCONFIRM=false

Storage Security

Bucket Policies

Restrict file access with policies:
-- Create private bucket
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', false);

-- Users can upload their own avatar
CREATE POLICY "Users can upload avatar"
  ON storage.objects FOR INSERT
  WITH CHECK (
    bucket_id = 'avatars' AND
    auth.uid()::text = (storage.foldername(name))[1]
  );

-- Users can read their own avatar
CREATE POLICY "Users can read avatar"
  ON storage.objects FOR SELECT
  USING (
    bucket_id = 'avatars' AND
    auth.uid()::text = (storage.foldername(name))[1]
  );

File Upload Restrictions

.env
# Limit file size (50MB)
FILE_SIZE_LIMIT=52428800

# Restrict MIME types
ALLOWED_MIME_TYPES=image/png,image/jpeg,image/gif,application/pdf

Monitoring & Auditing

Audit Logging

Track security events:
-- Create audit log table
CREATE TABLE audit_log (
  id BIGSERIAL PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id),
  action TEXT NOT NULL,
  table_name TEXT,
  record_id BIGINT,
  old_data JSONB,
  new_data JSONB,
  ip_address INET,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Audit trigger function
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO audit_log (user_id, action, table_name, record_id, old_data, new_data)
  VALUES (
    auth.uid(),
    TG_OP,
    TG_TABLE_NAME,
    NEW.id,
    to_jsonb(OLD),
    to_jsonb(NEW)
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Apply to sensitive tables
CREATE TRIGGER audit_users
  AFTER INSERT OR UPDATE OR DELETE ON users
  FOR EACH ROW EXECUTE FUNCTION audit_trigger();

Failed Login Monitoring

-- Track failed login attempts
CREATE TABLE failed_logins (
  id BIGSERIAL PRIMARY KEY,
  email TEXT,
  ip_address INET,
  attempted_at TIMESTAMPTZ DEFAULT NOW()
);

-- Alert on multiple failures
CREATE OR REPLACE FUNCTION check_failed_logins()
RETURNS TRIGGER AS $$
DECLARE
  failure_count INTEGER;
BEGIN
  SELECT COUNT(*) INTO failure_count
  FROM failed_logins
  WHERE email = NEW.email
    AND attempted_at > NOW() - INTERVAL '15 minutes';
  
  IF failure_count >= 5 THEN
    -- Block or alert
    RAISE EXCEPTION 'Too many failed login attempts';
  END IF;
  
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Incident Response

Security Breach Procedure

1

Contain the Breach

  • Immediately rotate all secrets
  • Block compromised IP addresses
  • Revoke compromised API keys
  • Disable affected user accounts
2

Assess Impact

  • Review audit logs
  • Identify affected data
  • Check for data exfiltration
  • Document timeline
3

Remediate

  • Apply security patches
  • Fix vulnerabilities
  • Restore from clean backup if needed
  • Update security policies
4

Notify Stakeholders

  • Alert affected users
  • Report to authorities if required
  • Update security documentation

Emergency Access Revocation

# Revoke all active sessions
docker exec supabase-db psql -U postgres -d postgres <<EOF
DELETE FROM auth.sessions;
DELETE FROM auth.refresh_tokens;
EOF

# Rotate JWT secret
sh ./utils/generate-keys.sh

# Restart auth service
docker compose restart auth

Security Checklist

Before going to production:
  • All default passwords changed
  • Strong, unique secrets generated
  • HTTPS enabled with valid SSL certificate
  • Firewall configured
  • Database backups automated
  • RLS enabled on all user tables
  • SMTP configured for email
  • OAuth providers verified
  • File upload restrictions set
  • Rate limiting configured
  • Monitoring and alerts set up
  • Audit logging enabled
  • Incident response plan documented
  • Regular security updates scheduled

Next Steps

Updates

Keep your stack secure with regular updates

Backups

Implement backup and recovery

Monitoring

Set up security monitoring

Compliance

Meet regulatory requirements