nginx

Nginx SSL/TLS Termination Configuration

Secure Nginx SSL/TLS termination config with modern cipher suites, HSTS, OCSP stapling, and A+ SSL Labs rating.

Overview

SSL/TLS termination handles encryption at the Nginx layer so backend services receive plain HTTP. This configuration uses modern security defaults, achieves an A+ rating on SSL Labs, and includes OCSP stapling for faster certificate verification.

Configuration

# /etc/nginx/conf.d/ssl.conf

# SSL session settings (shared across all server blocks)
ssl_session_cache shared:SSL:10m;     # 10MB shared cache (~40,000 sessions)
ssl_session_timeout 1d;               # Sessions valid for 1 day
ssl_session_tickets off;              # Disable tickets for forward secrecy

# Modern TLS protocols only
ssl_protocols TLSv1.2 TLSv1.3;       # Drop TLSv1.0 and TLSv1.1

# Cipher suite configuration
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;        # Let client choose (TLSv1.3 handles this)

# OCSP stapling — server fetches certificate status instead of client
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;  # DNS resolver for OCSP
resolver_timeout 5s;

# Diffie-Hellman parameter for DHE ciphers
ssl_dhparam /etc/nginx/dhparam.pem;   # Generate: openssl dhparam -out /etc/nginx/dhparam.pem 2048
# /etc/nginx/sites-available/secure-app.conf

server {
    listen 80;
    server_name example.com www.example.com;

    # ACME challenge for Let's Encrypt renewal
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # Redirect everything else to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    # Certificate files
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # HSTS — tell browsers to always use HTTPS (2 years)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # Additional security headers
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;

    # Proxy to backend
    location / {
        proxy_pass http://127.0.0.1:3000;
        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;
    }
}

Key Options Explained

  • ssl_session_cache shared:SSL:10m — Shares TLS session data across worker processes, allowing session resumption without a full handshake.
  • ssl_session_tickets off — Disabling session tickets ensures forward secrecy is maintained. Session cache still provides resumption.
  • ssl_stapling on — Nginx fetches the OCSP response from the CA and sends it during the TLS handshake, saving the client a round trip.
  • ssl_dhparam — A custom Diffie-Hellman group prevents common DH attacks. Generate with openssl dhparam -out dhparam.pem 2048.
  • HSTS preload — Once submitted to the HSTS preload list, browsers will never connect over HTTP. Only enable after confirming HTTPS works.

Common Modifications

  • Wildcard certificates: Use *.example.com certs with server_name *.example.com for subdomains.
  • Client certificate auth: Add ssl_client_certificate /path/to/ca.pem; and ssl_verify_client on; for mutual TLS.
  • TLSv1.3 only: Set ssl_protocols TLSv1.3; if you only need to support modern browsers.
  • Certificate auto-renewal: Use certbot with --webroot -w /var/www/certbot and a cron job or systemd timer.