Skip to main content

Nginx SSL/TLS Configuration: Complete Guide

Nginx is one of the most widely used HTTPS servers thanks to its high-performance TLS handling. This page covers the key directives — ssl_certificate, ssl_protocols, ssl_ciphers — and walks through a complete HTTPS setup including HTTP→HTTPS redirects.


Basic HTTPS Configuration

# /etc/nginx/conf.d/https.conf

server {
listen 443 ssl;
listen [::]:443 ssl; # IPv6
http2 on; # HTTP/2 (Nginx 1.25.1+)
server_name example.com www.example.com;

# Certificate and private key
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

location / {
root /var/www/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;
}

TLS Version and Cipher Suite

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

ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;

# TLS versions: allow 1.2 and 1.3 only
ssl_protocols TLSv1.2 TLSv1.3;

# Cipher suites (for TLS 1.2; TLS 1.3 is configured automatically)
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:'
'!DSS';

# Server cipher preference (off is recommended for TLS 1.3)
ssl_prefer_server_ciphers off;

# ECDH parameters
ssl_ecdh_curve X25519:prime256v1:secp384r1;

# DH parameters (for TLS 1.2 DHE)
# ssl_dhparam /etc/nginx/dhparam.pem; # openssl dhparam -out dhparam.pem 4096
}

TLS 1.3 Only (highest security, blocks some older clients)

ssl_protocols TLSv1.3;
# TLS 1.3 cipher suites cannot be set directly — OpenSSL selects automatically

SSL Session Cache (Performance)

TLS handshakes are computationally expensive. Session caching skips the handshake on reconnection.

# Set in the http block of nginx.conf or a conf.d/ file
http {
# Shared session cache: ~4000 sessions per MB
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d; # Session lifetime: 1 day
ssl_session_tickets off; # Disable for security (protects Forward Secrecy)
}

OCSP Stapling

Normally the browser checks certificate validity by querying an OCSP server on every connection. OCSP Stapling has the server pre-fetch the OCSP response and deliver it to the client, eliminating the extra round trip.

server {
ssl_stapling on;
ssl_stapling_verify on;

# CA chain used to verify the OCSP response
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

# DNS resolver for the OCSP server lookup
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
}

Complete Production Nginx HTTPS Configuration

# /etc/nginx/conf.d/example.com.conf

# HTTP → HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;

# Exclude Let's Encrypt renewal path
location /.well-known/acme-challenge/ {
root /var/www/html;
}

location / {
return 301 https://$host$request_uri;
}
}

# HTTPS server
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com www.example.com;

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

# TLS settings
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';
ssl_prefer_server_ciphers off;
ssl_ecdh_curve X25519:prime256v1:secp384r1;

# Session cache
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 1.1.1.1 valid=300s;
resolver_timeout 5s;

# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Application proxy
location / {
proxy_pass http://backend;
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;
}

# Logs
access_log /var/log/nginx/example.com-access.log;
error_log /var/log/nginx/example.com-error.log warn;
}

Validation

# Check config syntax
sudo nginx -t

# Apply changes
sudo systemctl reload nginx

# Verify TLS 1.3 is working
openssl s_client -connect example.com:443 -tls1_3 2>/dev/null | grep "Protocol"
# Protocol : TLSv1.3

# Verify TLS 1.0/1.1 are blocked
openssl s_client -connect example.com:443 -tls1 2>&1 | grep -E "handshake|alert"
# handshake failure ← correct, blocked

# Verify weak cipher is blocked
openssl s_client -connect example.com:443 -cipher 'RC4' 2>&1 | grep "cipher"
# no peer certificate available ← correct, blocked

# External test
# https://www.ssllabs.com/ssltest/analyze.html?d=example.com

www → non-www (or vice versa) Redirect

# www → example.com (prefer non-www)
server {
listen 443 ssl;
http2 on;
server_name www.example.com;

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

return 301 https://example.com$request_uri;
}

# Main domain
server {
listen 443 ssl;
http2 on;
server_name example.com;
# ... rest of config
}

The next page covers Apache mod_ssl HTTPS configuration.