Nginx TLS cheatsheet.
Basic HTTPS
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / { ... }
}
Modern TLS config
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 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
ssl_dhparam /etc/nginx/dhparams.pem;
Generate dhparam (one-time):
openssl dhparam -out /etc/nginx/dhparams.pem 2048
Or use Mozilla’s: https://ssl-config.mozilla.org
HTTP → HTTPS redirect
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
After confirmation: register at https://hstspreload.org for browser preload list.
certbot (Let’s Encrypt)
apt install certbot python3-certbot-nginx
# Auto-config nginx
certbot --nginx -d example.com -d www.example.com
# Cert-only (manual nginx config)
certbot certonly --webroot -w /var/www/html -d example.com
Renewal: certbot installs cron/timer. Verify:
certbot renew --dry-run
Webroot ACME challenge
location /.well-known/acme-challenge/ {
root /var/www/html;
}
Allow port 80 access for renewals even with HTTPS-only sites.
HTTP/2
listen 443 ssl http2;
HTTP/3 (QUIC)
listen 443 quic reuseport;
listen 443 ssl http2;
add_header Alt-Svc 'h3=":443"; ma=86400';
Requires nginx built with HTTP/3 support (nginx 1.25+).
Wildcard certificate
certbot certonly --manual --preferred-challenges dns -d "*.example.com" -d example.com
Requires DNS plugin (cloudflare, route53, etc) for automation:
certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/cf.ini -d "*.example.com"
mTLS (client certificates)
ssl_client_certificate /etc/nginx/client-ca.pem;
ssl_verify_client on;
location / {
if ($ssl_client_verify != "SUCCESS") { return 403; }
proxy_set_header X-Client-CN $ssl_client_s_dn_cn;
}
SNI multi-cert
Each server block has its own cert. SNI auto-routes:
server {
listen 443 ssl;
server_name a.example.com;
ssl_certificate /etc/letsencrypt/live/a.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/a.example.com/privkey.pem;
}
server {
listen 443 ssl;
server_name b.example.com;
ssl_certificate /etc/letsencrypt/live/b.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/b.example.com/privkey.pem;
}
TLS termination patterns
- Edge LB + nginx: LB does TLS, nginx is HTTP.
- Nginx-only: nginx does TLS.
- Nginx + backend TLS: re-encrypt to backend.
Security headers (with TLS)
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;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'" always;
Snippet file
# /etc/nginx/snippets/ssl-modern.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ...;
ssl_session_cache shared:SSL:10m;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Include from server block:
include /etc/nginx/snippets/ssl-modern.conf;
Cert auto-reload
nginx -s reload after certbot renew (hook):
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
nginx -s reload
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Testing
openssl s_client -connect example.com:443 -servername example.com -showcerts < /dev/null
curl -vI https://example.com
# https://www.ssllabs.com/ssltest/
# https://testssl.sh/
Common mistakes
- Old protocols (TLSv1.0/1.1) — disable.
- HSTS in dev → can’t access HTTP later (use
max-age=0to clear). - Wrong fullchain.pem (missing intermediate) → trust errors.
ssl_certificate_keyworld-readable → security issue.- Renewal failing silently → cert expires.
Read this next
If you want my TLS snippets + certbot setup, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .