Nginx handles SSL differently from Apache. There’s no module system to enable — it’s built in. But the configuration has some nuances that trip people up, especially around cipher suites, OCSP stapling, and HTTP/2. I’ve set up SSL on Nginx across dozens of servers, and this guide reflects what actually works in production, not just a minimal example.
Basic SSL Server Block
Let’s start with a working configuration and build from there.
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/private.key;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
The ssl_certificate file should be your full chain — your domain certificate followed by any intermediate certificates. Not just your domain cert alone. Missing intermediates is the most common reason SSL works in Chrome but fails in older Android or enterprise browsers that don’t have your CA’s intermediate cert cached.
ssl_certificate_key points to your private key. That file should be owned by root and have 600 permissions:
chmod 600 /etc/nginx/ssl/private.key
chown root:root /etc/nginx/ssl/private.key
Redirecting HTTP to HTTPS
You need a second server block to handle plain HTTP and redirect it:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
Using $host$request_uri preserves the full path on redirect, so someone visiting http://example.com/about lands on https://example.com/about, not just the homepage.
Some guides suggest using rewrite ^(.*)$ https://$host$1 permanent; instead. The return 301 approach is faster — Nginx processes it without regex evaluation.
TLS Protocol Versions and Cipher Suites
Outdated TLS protocol versions are a security risk. TLS 1.0 and 1.1 have known weaknesses and are disabled by most modern clients anyway. Here’s what to set:
ssl_protocols TLSv1.2 TLSv1.3;
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:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
The ssl_prefer_server_ciphers off setting lets the client choose from the cipher list rather than imposing server order. With TLS 1.3, the client’s preference generally leads to better performance on modern hardware. I keep it off unless there’s a specific reason to force server-side ordering.
If you need to support older clients (Android 4.x, IE 11 on Windows 7), you’ll need to add some older ciphers. But I’d push back on that requirement — supporting those clients means weakening security for everyone else.
SSL Session Cache and Resumption
TLS handshakes are expensive. Session caching dramatically reduces the overhead for returning visitors:
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
The shared:SSL:10m creates a 10MB cache shared across all Nginx worker processes. Each megabyte holds roughly 4,000 sessions, so 10MB handles about 40,000 concurrent sessions — more than enough for most deployments.
I set ssl_session_tickets off because session tickets have had security vulnerabilities in the past, and they require careful key rotation to maintain forward secrecy. For most setups, the session cache is sufficient and safer.
Diffie-Hellman Parameters
For DHE cipher suites (the ones starting with DHE-), you need to generate custom DH parameters. The default 1024-bit parameters built into many systems are too weak:
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
This takes a few minutes. Then add it to your server block:
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
If you’re only using ECDHE ciphers (which most modern configs do), this isn’t strictly required. But including it doesn’t hurt and improves your SSL test scores.
OCSP Stapling
Without OCSP stapling, browsers make a separate request to the CA’s OCSP server to check whether your certificate has been revoked. This adds latency and leaks browsing data to the CA. With stapling, Nginx handles this check in the background and includes the OCSP response directly in the TLS handshake.
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
The ssl_trusted_certificate is the CA chain (intermediate + root certs, without your domain cert). Some distributions combine this into the fullchain file, but for OCSP stapling specifically, you want just the CA certs.
After restarting Nginx, you can verify OCSP stapling is working:
openssl s_client -connect example.com:443 -status -servername example.com 2>&1 | grep -A 17 'OCSP response'
You should see OCSP Response Status: successful and the response details.
HTTP Strict Transport Security (HSTS)
HSTS tells browsers that this domain should only ever be accessed over HTTPS, even if someone types http://:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Start with a short max-age (300 seconds) while testing, then increase to a year once you’re sure everything works. The always directive ensures the header is added even on error responses.
Be careful with includeSubDomains — it applies to all subdomains, including ones you haven’t configured for HTTPS yet. If you have ftp.example.com without SSL, HSTS will break it in browsers.
Enabling HTTP/2
HTTP/2 multiplexes multiple requests over a single connection, which significantly improves page load performance for sites with many resources.
listen 443 ssl http2;
listen [::]:443 ssl http2;
Just add http2 to the listen directive. Nginx requires SSL to use HTTP/2 (because browsers only support HTTP/2 over TLS in practice).
Check that it’s working:
curl -I --http2 https://example.com
You should see HTTP/2 200 in the response.
Complete Production Configuration
Putting it all together:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/private.key;
ssl_trusted_certificate /etc/nginx/ssl/chain.pem;
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
ssl_protocols TLSv1.2 TLSv1.3;
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:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
Testing Your Configuration
Always test before reloading:
nginx -t
If that passes:
systemctl reload nginx
Use reload rather than restart — it applies the new configuration without dropping existing connections.
For external validation, SSL Labs gives a detailed report and a letter grade. Aim for A+. The configuration above will get you there on a fresh server. If you’re scoring B or lower, it’s almost always the protocol versions or cipher suites.
Moving SSL Config to a Shared File
If you’re running multiple virtual hosts, repeating the SSL directives in every server block gets messy. Extract them into a shared file:
# /etc/nginx/snippets/ssl-params.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:...;
ssl_session_cache shared:SSL:10m;
# ... rest of SSL settings
Then include it in each server block:
server {
listen 443 ssl http2;
include /etc/nginx/snippets/ssl-params.conf;
ssl_certificate /etc/nginx/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.com/private.key;
# ... site-specific config
}
This way, updating your cipher list or enabling a new feature happens in one place and applies everywhere.