SSL Certificate Placement Guide for Nginx Reverse Proxy: Frontend vs Backend Configuration


2 views

When implementing SSL/TLS with Nginx reverse proxy, you need to consider two distinct certificate scenarios:

  1. Frontend certificates: Used between client browsers and Nginx
  2. Backend certificates: Used between Nginx and upstream servers

For your domain-facing Nginx server (x.x.x.x), you should install the actual domain certificates (example1.com, example2.com, etc.). This ensures clients see valid certificates for the domains they're accessing.

server {
    listen 443 ssl;
    server_name example1.com;
    
    ssl_certificate /etc/nginx/ssl/example1.com/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/example1.com/privkey.pem;
    
    location / {
        proxy_pass https://a.b.c.d:1234;
        proxy_ssl_verify on;
        proxy_ssl_trusted_certificate /etc/nginx/ssl/upstream_ca.pem;
        proxy_ssl_name example1.com;
    }
}

For upstream servers (a.b.c.d:1234), you have several valid approaches:

  • Option 1: Use the same domain certificate (not recommended for security)
  • Option 2: Generate new certificates signed by internal CA
  • Option 3: Use self-signed certificates with proper validation

The most secure approach is to create an internal CA and issue separate certificates for backend servers:

# On upstream server a.b.c.d:1234 (example1-backend)
ssl_certificate /etc/ssl/certs/example1-backend.crt;
ssl_certificate_key /etc/ssl/private/example1-backend.key;

Then configure Nginx to verify these certificates:

proxy_ssl_certificate /etc/nginx/ssl/client.crt;
proxy_ssl_certificate_key /etc/nginx/ssl/client.key;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/ssl/ca.crt;
proxy_ssl_verify_depth 2;

For production environments, implement these security measures:

  • Use separate certificates for frontend and backend
  • Enable OCSP stapling on frontend
  • Implement certificate pinning for critical services
  • Rotate certificates regularly (automate with ACME)
  • Monitor certificate expiration dates

Here's a full working example for example1.com:

# Frontend configuration
server {
    listen 443 ssl http2;
    server_name example1.com;
    
    # Public certificate
    ssl_certificate /etc/letsencrypt/live/example1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example1.com/privkey.pem;
    
    # Security headers
    add_header Strict-Transport-Security "max-age=31536000" always;
    
    location / {
        proxy_pass https://a.b.c.d:1234;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        
        # Upstream SSL verification
        proxy_ssl_certificate /etc/nginx/ssl/client.crt;
        proxy_ssl_certificate_key /etc/nginx/ssl/client.key;
        proxy_ssl_verify on;
        proxy_ssl_trusted_certificate /etc/nginx/ssl/internal-ca.crt;
        proxy_ssl_verify_depth 2;
        proxy_ssl_session_reuse on;
    }
}

If encountering SSL errors between Nginx and upstream:

  1. Check time synchronization (certificates are time-sensitive)
  2. Verify the CA chain is complete
  3. Ensure the upstream server's certificate includes proper SANs
  4. Inspect Nginx error logs at /var/log/nginx/error.log
  5. Test with openssl s_client -connect a.b.c.d:1234 -showcerts

When configuring SSL certificates in an Nginx reverse proxy setup, we need to distinguish between two distinct TLS termination points:

  • Client-facing termination: Between end users and your proxy server
  • Upstream termination: Between your proxy and backend servers

For your frontend configuration (client ↔ nginx), you'll need:

server {
    listen 443 ssl;
    server_name example1.com;
    
    ssl_certificate /etc/nginx/ssl/example1.com/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/example1.com/privkey.pem;
    
    location / {
        proxy_pass https://a.b.c.d:1234;
        proxy_ssl_verify on;
        proxy_ssl_trusted_certificate /etc/nginx/ssl/ca-certs.pem;
    }
}

The client-facing certificate must match the domain name users access (example1.com). This is non-negotiable for browser trust.

For the upstream connection (nginx ↔ backend), you have three viable approaches:

Option 1: Reuse Domain Certificates (Simplest)

# On backend server a.b.c.d:1234
ssl_certificate /path/to/example1.com.crt;
ssl_certificate_key /path/to/example1.com.key;

This works when your proxy can resolve the backend's certificate CN/SANs.

Option 2: Internal CA-Signed Certificates (More Secure)

# Generate for each backend
openssl req -newkey rsa:2048 -nodes -keyout backend1.key \
  -subj "/CN=backend1.internal" -out backend1.csr

# Sign with internal CA
openssl x509 -req -in backend1.csr -CA internal-ca.crt \
  -CAkey internal-ca.key -CAcreateserial -out backend1.crt

Configure Nginx to trust your internal CA:

proxy_ssl_trusted_certificate /etc/nginx/ssl/internal-ca.crt;
proxy_ssl_verify on;

Option 3: Self-Signed with Certificate Pinning

# Generate self-signed
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
  -keyout backend.key -out backend.crt \
  -subj "/CN=backend1.internal" \
  -addext "subjectAltName=DNS:backend1.internal"

# Configure Nginx
proxy_ssl_certificate /etc/nginx/ssl/backend1.crt;
proxy_ssl_certificate_key /etc/nginx/ssl/backend1.key;
proxy_ssl_verify off; # We're pinning instead
proxy_ssl_verify_depth 2;
proxy_ssl_server_name on;

Here's a complete setup using internal CA certificates:

# nginx.conf fragment
upstream backend_example1 {
    server a.b.c.d:1234;
    keepalive 16;
}

server {
    listen 443 ssl http2;
    server_name example1.com;
    
    # Client-facing cert
    ssl_certificate /etc/letsencrypt/live/example1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example1.com/privkey.pem;
    
    # SSL optimizations
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    
    location / {
        proxy_pass https://backend_example1;
        
        # Upstream SSL config
        proxy_ssl_certificate /etc/nginx/ssl/client.crt;
        proxy_ssl_certificate_key /etc/nginx/ssl/client.key;
        proxy_ssl_trusted_certificate /etc/nginx/ssl/internal-ca.crt;
        proxy_ssl_verify on;
        proxy_ssl_protocols TLSv1.2 TLSv1.3;
        
        # Standard proxy headers
        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;
    }
}
  • Use separate certificates for frontend and backend communication
  • Automate renewals with tools like certbot for client-facing certs
  • Maintain a CRL (Certificate Revocation List) for internal certificates
  • Set appropriate certificate lifetimes (90 days for public, 1 year for internal)
  • Implement OCSP stapling for client-facing certificates