A Modern nginx TLS Configuration
Configure nginx with TLS 1.2/1.3, Mozilla Intermediate cipher suites, OCSP stapling, HSTS, and DH parameters for a production-ready HTTPS setup.
Before you start
- ▸nginx installed and serving at least one HTTP virtual host
- ▸A valid TLS certificate and private key (Let's Encrypt or commercial CA)
- ▸Root or sudo access on the server
- ▸Outbound DNS resolution available from the server (required for OCSP stapling)
Getting TLS right on nginx means more than just pointing at your certificate files. A misconfigured server can still negotiate weak cipher suites, skip OCSP stapling, or omit the HSTS header that stops protocol-downgrade attacks. This guide walks you through a production-grade nginx TLS configuration based on the Mozilla Intermediate compatibility profile — the right default for most public-facing servers today.
Prerequisites and Assumptions
These steps assume nginx is already installed and serving HTTP traffic. You have a valid certificate and private key — either from Let's Encrypt (certbot) or another CA. The configuration paths below follow the standard layout on each distro. Run all commands as root or with sudo.
Understanding the Mozilla Intermediate Profile
Mozilla publishes the SSL Configuration Generator, which defines three profiles: Modern, Intermediate, and Old. Intermediate is the practical sweet spot — it drops TLS 1.0 and 1.1, requires TLS 1.2 and 1.3, and excludes known-weak cipher suites while still supporting clients as old as Firefox 27 and Android 4.4.2. Modern requires TLS 1.3 only and excludes a large swath of real-world clients. Old is for legacy systems you should be migrating away from.
Step 1: Locate Your nginx Configuration
The main configuration file and the vhost/server-block layout differ slightly by distro.
- Debian/Ubuntu:
/etc/nginx/nginx.conf, server blocks in/etc/nginx/sites-available/, symlinked into/etc/nginx/sites-enabled/ - Fedora/RHEL/Rocky:
/etc/nginx/nginx.conf, drop-in files in/etc/nginx/conf.d/ - Arch:
/etc/nginx/nginx.conf, include files from/etc/nginx/conf.d/or inline server blocks
Put shared TLS settings in a snippet file you can include from every HTTPS server block.
sudo mkdir -p /etc/nginx/snippets
Step 2: Create a Shared TLS Snippet
Create /etc/nginx/snippets/tls-intermediate.conf. Every directive here is explained below the block.
sudo nano /etc/nginx/snippets/tls-intermediate.conf
# TLS protocol versions
ssl_protocols TLSv1.2 TLSv1.3;
# Cipher suite list (Mozilla Intermediate, openssl format)
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:DHE-RSA-CHACHA20-POLY1305;
# Let the server choose cipher order (matters more for TLS 1.2)
ssl_prefer_server_ciphers off;
# TLS session cache — shared across worker processes
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# DH parameters for DHE cipher suites
ssl_dhparam /etc/nginx/dhparam.pem;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# HSTS — 2-year max-age, include subdomains, preload-ready
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
What Each Directive Does
- ssl_protocols: Explicitly allows only TLS 1.2 and 1.3. TLS 1.0 and 1.1 are deprecated by RFC 8996 and disabled by default in OpenSSL 3.x, but being explicit prevents surprises if nginx was compiled against an older library.
- ssl_ciphers: All suites use ECDHE or DHE for forward secrecy, AESGCM or ChaCha20-Poly1305 for authenticated encryption. No RC4, 3DES, CBC-mode, or static RSA key exchange. For TLS 1.3, OpenSSL manages its own cipher list separately; this directive only controls TLS 1.2 suites.
- ssl_prefer_server_ciphers off: Mozilla's Intermediate profile now sets this to
off. With modern clients, the client's preference ordering is usually fine, and this avoids blocking clients that prefer ChaCha20 on devices without hardware AES acceleration. - ssl_session_cache / ssl_session_timeout: Server-side session resumption cache shared across all nginx workers, with a one-day lifetime. Reduces full-handshake overhead on repeat connections.
- ssl_session_tickets off: Session tickets require careful rotation of the ticket encryption key. Disabling them avoids forward-secrecy concerns unless you have a proper key-rotation mechanism in place.
- ssl_stapling: Instructs nginx to fetch and cache the OCSP response from the CA, then staple it to the TLS handshake. Clients get revocation status without making a separate OCSP request to the CA, which is faster and more private.
- resolver: Required for OCSP stapling so nginx can look up the CA's OCSP responder hostname. Use your own resolver in air-gapped environments.
- Strict-Transport-Security: Tells browsers to never connect over plain HTTP for the next two years. The
preloaddirective qualifies the domain for submission to browser HSTS preload lists — only add it if you are sure all subdomains are HTTPS-ready. Thealwaysflag ensures the header is sent on error responses too.
Step 3: Generate DH Parameters
DHE cipher suites need a DH parameter file. Generate a 2048-bit file — this is the Mozilla-recommended size; 4096 bits provides no practical security gain and adds significant CPU cost during handshakes.
sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048
This takes 20–60 seconds. Do it once; the file does not need regular rotation.
Step 4: Configure Your Server Block
Reference the snippet and your certificate files inside your HTTPS server block.
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on; # nginx >= 1.25.1; older versions: use 'listen 443 ssl http2'
server_name example.com www.example.com;
# Certificate paths (Let's Encrypt example)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Pull in all shared TLS settings
include snippets/tls-intermediate.conf;
# For OCSP stapling verification — the CA chain bundle
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
root /var/www/example.com/html;
index index.html;
}
# HTTP → HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
ssl_trusted_certificate is separate from ssl_certificate — it points to the CA chain used to verify the OCSP response signature, not the chain sent to clients. With Let's Encrypt, chain.pem is the right file. With other CAs, use the intermediate CA certificate bundle.
Step 5: Test and Reload nginx
sudo nginx -t
Expected output (exact version numbers will vary):
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx
Step 6: Verify the Configuration
Using testssl.sh
testssl.sh is the most thorough command-line TLS auditing tool available.
# Debian/Ubuntu
sudo apt install testssl.sh
# Fedora/RHEL
sudo dnf install testssl
# Arch
sudo pacman -S testssl.sh
testssl.sh https://example.com
Look for: TLS 1.0/1.1 reported as not offered, TLS 1.3 offered, OCSP stapling offered, HSTS yes, no CRIME/BEAST/POODLE/ROBOT findings.
Using OpenSSL
Quick check for OCSP stapling specifically:
openssl s_client -connect example.com:443 -tls1_2 -status -servername example.com < /dev/null 2>&1 | grep -A5 "OCSP response"
A successful staple shows OCSP Response Status: successful (0x0). If it shows no response sent, nginx may not have fetched the OCSP response yet — wait 60 seconds and retry, since the first staple is fetched lazily.
Troubleshooting
- OCSP stapling not working: Confirm
ssl_trusted_certificatepoints to the CA chain, not the full chain. Check that theresolveris reachable from the server — outbound UDP/TCP port 53 must be open. Errors appear in/var/log/nginx/error.logtagged[warn]with "OCSP_basic_verify" or "no resolver defined". - nginx -t fails on ssl_dhparam: The
dhparam.pemfile must exist before nginx starts. If you created it after the initial install, ensure the path matches exactly what is in the snippet. - http2 on directive unknown: You are running nginx older than 1.25.1. Replace
http2 on;withlisten 443 ssl http2;on the listen line instead. - HSTS locking out subdomains: If a subdomain is not yet HTTPS-ready, remove
includeSubDomainsfrom the header until it is. Do not submit to the preload list until every subdomain serves valid HTTPS. - Grade lower than A on SSL Labs: Run the Qualys SSL Labs test at ssllabs.com. A common culprit is an incomplete certificate chain — make sure
ssl_certificatepoints tofullchain.pem, not justcert.pem.
Frequently asked questions
- Why set ssl_prefer_server_ciphers to off?
- Mozilla's current Intermediate profile sets it to off because modern clients have sensible cipher preferences. Devices without AES hardware acceleration prefer ChaCha20-Poly1305 for performance, and overriding that preference with a server-side order provides no real security benefit when the full list already excludes weak suites.
- Do I need to renew dhparam.pem periodically?
- No. DH parameters are not secret material and do not expire. A 2048-bit file generated once is fine indefinitely. Regenerating it provides no forward-secrecy benefit.
- Is it safe to add the HSTS preload directive immediately?
- Only if every subdomain under your domain already serves valid HTTPS. Preloading is permanent and hard to undo — browsers ship with the list baked in. Start without includeSubDomains and preload, confirm everything works, then add them incrementally.
- Why does OCSP stapling show 'no response sent' on the first request?
- nginx fetches the OCSP response lazily after the first request triggers it, then caches it. Wait about 60 seconds after reloading nginx and test again. If it still fails, check /var/log/nginx/error.log for resolver or certificate chain errors.
- Does this configuration work with self-signed certificates?
- The ssl_protocols and ssl_ciphers settings work with any certificate, but OCSP stapling requires a certificate issued by a CA with a public OCSP responder. Disable ssl_stapling and ssl_stapling_verify for self-signed or internal-CA certificates.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.