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=0 to clear).
  • Wrong fullchain.pem (missing intermediate) → trust errors.
  • ssl_certificate_key world-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 .