LFCE 48 ๐ง Nginx โ TLS Configuration
TLS configuration is the step that turns an HTTP server into an HTTPS server. Nginx handles TLS through the ngx_http_ssl_module, which is compiled into the standard packages but must be enabled by adding the ssl parameter to the listen directive and pointing ssl_certificate and ssl_certificate_key at the certificate and private key files. The previous chapter covered virtual hosts; this chapter adds the encryption layer that protects the traffic those hosts serve.
The configuration has two parts that are often conflated. The first is the certificate itself: where it comes from, how it is chained with intermediate certificates, and how it is renewed. The second is the TLS policy: which protocol versions are permitted, which cipher suites are offered, and which security headers accompany the response. A certificate without a policy is a server that encrypts, but may encrypt with obsolete protocols or weak ciphers. A policy without a certificate is a server that refuses to start.
Key point: The ssl_certificate directive must point to the full certificate chain, with the server certificate first and the intermediate certificates after it. If the server certificate is not first, nginx fails to start with an SSL_CTX_use_PrivateKey_file error because it tries to match the private key against the wrong certificate . Modern nginx defaults to TLS 1.2 and TLS 1.3, but the protocol and cipher directives should be set explicitly for clarity and to exclude legacy clients that the default might otherwise accept .
Why TLS configuration requires certificate chain discipline
The chain-order problem.
A certificate authority signs the server certificate with an intermediate certificate, and the intermediate is signed by a root certificate that the browser trusts. The browser needs the intermediate to verify the chain, but many servers send only the server certificate. Some browsers cache intermediates from previous connections and accept the incomplete chain; others do not, and the user sees a warning. The fix is to concatenate the server certificate and the intermediate bundle into a single file, with the server certificate first, and to point ssl_certificate at that combined file . The order is not cosmetic: if the bundle comes first, nginx attempts to use the private key with the bundle’s first certificate and refuses to start.
The protocol-version problem.
TLS 1.0 and TLS 1.1 are deprecated and vulnerable to known attacks. TLS 1.2 is the minimum acceptable version for modern deployments, and TLS 1.3 is preferred because it removes obsolete cipher suites and reduces handshake latency. Nginx versions since 1.23.4 default to TLSv1.2 TLSv1.3, but older versions and some distribution builds still include TLS 1.0 and 1.1. The ssl_protocols directive should be set explicitly to TLSv1.2 TLSv1.3 so that the policy does not depend on the build’s default .
The cipher-suite problem.
TLS 1.2 allows the client and server to negotiate a cipher suite, and some suites are weak. The ssl_ciphers directive restricts the offered suites. For TLS 1.3, the cipher suites are fixed by the protocol and cannot be configured, so the ssl_ciphers directive affects only TLS 1.2 and earlier. A modern configuration uses a curated list that prefers ECDHE for forward secrecy and AES-GCM or ChaCha20-Poly1305 for authenticated encryption .
a. The basic TLS server block
The minimum TLS configuration adds three directives to an existing server block: the listen with ssl, the ssl_certificate, and the ssl_certificate_key.
server {
listen 443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/example.com.chained.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /var/www/example;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
The listen 443 ssl; directive binds the block to the HTTPS port and enables TLS processing. The ssl_certificate points to the chained certificate file, and ssl_certificate_key points to the private key. The ssl_protocols directive restricts the accepted versions, and ssl_ciphers restricts the TLS 1.2 suites .
A separate server block on port 80 handles the HTTP-to-HTTPS redirect. Without it, clients that type http://example.com receive a connection error or the default site instead of a redirect.
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
The return 301 directive sends a permanent redirect, and $host preserves the hostname the client requested. This is the standard pattern for forcing HTTPS while keeping the HTTP port open for the ACME challenge that certificate renewal requires.
b. TLS policy and security headers
The protocol and cipher directives set the encryption policy. The security headers tell the browser to enforce it.
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com.chained.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
# Protocol policy
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers off;
# Session resumption
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/nginx/ssl/example.com.chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
# HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
}
The ssl_prefer_server_ciphers off setting is correct for modern clients because it allows the client to choose from the server’s list, and TLS 1.3 clients ignore the list anyway. The session cache stores session parameters so that returning clients do not repeat the full handshake; shared:SSL:10m allocates a 10-megabyte cache shared across worker processes . The ssl_session_tickets off directive disables session tickets, which can be a security risk if the ticket key is not rotated.
OCSP stapling allows the server to fetch the certificate’s revocation status and include it in the handshake, so the client does not need to contact the certificate authority separately. The ssl_trusted_certificate directive is required for stapling verification; without it, nginx cannot validate the OCSP response it receives . The resolver directive tells nginx which DNS servers to use for the OCSP lookup.
The Strict-Transport-Security header tells the browser to use HTTPS for all future requests to the domain, even if the user types http://. The max-age value is in seconds; 63072000 is two years. The includeSubDomains directive extends the policy to subdomains. The preload token is omitted here because it requires submission to the browser preload list and is difficult to reverse .
c. Obtaining and renewing certificates with Certbot
The certificates themselves come from a certificate authority. Let’s Encrypt is the free, automated option, and Certbot is its client. The Certbot nginx plugin modifies the server block automatically, adding the ssl_certificate and ssl_certificate_key directives and the HTTP-to-HTTPS redirect.
# Debian / Ubuntu
sudo apt install certbot python3-certbot-nginx
# RHEL / Rocky (EPEL required)
sudo dnf install certbot python3-certbot-nginx
# Obtain and install the certificate
sudo certbot --nginx -d example.com -d www.example.com
The --nginx flag tells Certbot to use the nginx plugin, which reads the nginx configuration to find the server_name and modifies it in place. The -d flags specify the domain names; both the apex and the www subdomain should be included so that both are covered by the certificate. Certbot validates domain ownership by placing a file at /.well-known/acme-challenge/ and asking Let’s Encrypt to fetch it over HTTP on port 80 . This is why port 80 must remain open even after HTTPS is configured: the renewal uses the same challenge.
Certbot installs a systemd timer that runs twice daily and renews certificates that are within 30 days of expiry. The timer can be checked and tested manually.
# Check the timer
sudo systemctl status certbot-renew.timer
# Test renewal without consuming rate limits
sudo certbot renew --dry-run
The --dry-run flag performs the renewal process against the Let’s Encrypt staging environment, which has much higher rate limits than production. Running it before the first real renewal confirms that the challenge path and the plugin configuration are correct . If the dry run succeeds, the automatic renewal will succeed.
Complete Example Session
# ============================================
# PART 1: INSTALL CERTBOT
# ============================================
sudo apt install certbot python3-certbot-nginx
# ============================================
# PART 2: ENSURE THE SERVER BLOCK HAS A server_name
# ============================================
# Certbot needs a server_name to match the domain
# The block must be listening on port 80
sudo tee /etc/nginx/sites-available/example.com.conf > /dev/null <<'EOF'
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example;
index index.html;
}
EOF
sudo ln -sf /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# ============================================
# PART 3: OBTAIN AND INSTALL THE CERTIFICATE
# ============================================
sudo certbot --nginx -d example.com -d www.example.com
# Certbot modifies the server block:
# - Adds listen 443 ssl
# - Adds ssl_certificate and ssl_certificate_key
# - Adds HTTP-to-HTTPS redirect
# ============================================
# PART 4: INSPECT THE CERTBOT-MODIFIED BLOCK
# ============================================
cat /etc/nginx/sites-available/example.com.conf
# Output includes:
# listen 443 ssl; # managed by Certbot
# ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# include /etc/letsencrypt/options-ssl-nginx.conf;
# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# ============================================
# PART 5: ADD EXPLICIT TLS POLICY
# ============================================
# The options-ssl-nginx.conf file sets ssl_protocols and ssl_ciphers
# For clarity, set them explicitly in the server block
sudo tee -a /etc/nginx/sites-available/example.com.conf > /dev/null <<'EOF'
# Explicit TLS policy
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
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;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
EOF
# ============================================
# PART 6: VALIDATE AND RELOAD
# ============================================
sudo nginx -t
sudo systemctl reload nginx
# ============================================
# PART 7: TEST THE CERTIFICATE
# ============================================
curl -I https://example.com
# Output: HTTP/2 200 (or HTTP/1.1 200)
# The connection is encrypted with the Let's Encrypt certificate
# ============================================
# PART 8: VERIFY THE CHAIN
# ============================================
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates -subject
# Output shows the certificate subject and validity dates
# ============================================
# PART 9: TEST RENEWAL
# ============================================
sudo certbot renew --dry-run
# Output: Congratulations, all renewals succeeded
# ============================================
# PART 10: CHECK THE RENEWAL TIMER
# ============================================
sudo systemctl status certbot-renew.timer
# Output: active (waiting)
The ten parts demonstrate the complete TLS configuration: Certbot installation, server block preparation, certificate issuance, inspection of the Certbot-modified block, explicit TLS policy addition, validation, HTTPS testing, chain verification, renewal testing, and timer verification. The critical steps are Part 5 (explicit policy) and Part 9 (renewal dry run), which confirm that the encryption is configured correctly and will remain valid.
Quick Reference
TLS Directives
| Directive | Purpose | Example |
|---|---|---|
listen 443 ssl | Enable TLS on port 443 | listen 443 ssl; |
ssl_certificate | Certificate chain file | /etc/letsencrypt/live/.../fullchain.pem |
ssl_certificate_key | Private key file | /etc/letsencrypt/live/.../privkey.pem |
ssl_protocols | Accepted TLS versions | TLSv1.2 TLSv1.3 |
ssl_ciphers | TLS 1.2 cipher suites | HIGH:!aNULL:!MD5 |
ssl_session_cache | Session cache | shared:SSL:10m |
ssl_stapling | OCSP stapling | on |
ssl_trusted_certificate | Chain for stapling verification | chain.pem |
add_header HSTS | Force HTTPS in browser | max-age=63072000 |
Certbot Commands
| Command | Purpose |
|---|---|
certbot --nginx -d <domain> | Obtain and install certificate |
certbot certificates | List managed certificates |
certbot renew --dry-run | Test renewal without rate limits |
systemctl status certbot-renew.timer | Check renewal timer |
Certificate Files
| File | Content |
|---|---|
fullchain.pem | Server certificate + intermediates |
privkey.pem | Private key |
chain.pem | Intermediates only |
cert.pem | Server certificate only |
Best Practices
โ Do This:
# Use the full chain for ssl_certificate
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # โ
# Set explicit protocol versions
ssl_protocols TLSv1.2 TLSv1.3; # โ
# Enable OCSP stapling with trusted certificate
ssl_stapling on; ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; # โ
# Add HSTS header
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; # โ
# Keep port 80 open for ACME challenges
# The HTTP-to-HTTPS redirect must exempt /.well-known/acme-challenge/ # โ
# Test renewal before relying on it
certbot renew --dry-run # โ
โ Don’t Do This:
# Use the bare server certificate without the chain
ssl_certificate /etc/letsencrypt/live/example.com/cert.pem; # โ
# Allow obsolete TLS versions
ssl_protocols TLSv1 TLSv1.1; # โ
# Enable OCSP stapling without a resolver
ssl_stapling on; # No resolver โ stapling fails # โ
# Close port 80 after enabling HTTPS
# Renewal fails silently until the certificate expires # โ
# Use the preload HSTS token without understanding the consequences
add_header Strict-Transport-Security "...; preload"; # โ (difficult to reverse)
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Browser warns about incomplete chain | ssl_certificate points to bare cert | Use fullchain.pem or concatenated chain |
| Nginx fails to start with key mismatch | Chain concatenated in wrong order | Server cert first, then intermediates |
| Renewal fails silently | Port 80 closed | Keep port 80 open for ACME challenge |
| OCSP stapling does not work | No resolver or ssl_trusted_certificate | Add both directives |
| Certbot cannot find server block | server_name is _ or missing | Set the actual domain in server_name |
| HSTS preload causes lockout | preload submitted with misconfiguration | Omit preload until confirmed stable |
Real-World Examples
1. Basic TLS server block
server { listen 443 ssl; server_name example.com; ssl_certificate /path/fullchain.pem; ssl_certificate_key /path/privkey.pem; }
2. Protocol restriction
ssl_protocols TLSv1.2 TLSv1.3;
3. Cipher restriction
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
4. Session cache
ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d;
5. OCSP stapling
ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /path/chain.pem; resolver 8.8.8.8;
6. HSTS header
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
7. HTTP redirect
server { listen 80; server_name example.com; return 301 https://$host$request_uri; }
8. Certbot issuance
certbot --nginx -d example.com -d www.example.com
9. Renewal test
certbot renew --dry-run
10. Verify chain
openssl s_client -connect example.com:443 -servername example.com
Visual
Certificate Chain Order
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ssl_certificate MUST POINT TO THE CHAIN IN THIS ORDER โ
โ โ
โ fullchain.pem: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 1. Server certificate (example.com) โ โ
โ โ 2. Intermediate certificate (R3) โ โ
โ โ 3. (Optional) Root certificate โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โ
Correct order: server cert first โ
โ โ Wrong order: nginx fails to start with key mismatch โ
โ โ
โ The browser needs the intermediate to build the chain โ
โ from the server certificate to the trusted root. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
TLS Handshake with OCSP Stapling
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ OCSP STAPLING AVOIDS A SEPARATE CA LOOKUP โ
โ โ
โ Without stapling: โ
โ Client โโโถ Server: "Here is my cert" โ
โ Client โโโถ CA: "Is this cert still valid?" โ
โ CA โโโถ Client: "Yes" โ
โ โ
โ With stapling: โ
โ Server โโโถ CA (cached): "Is this cert still valid?" โ
โ Client โโโถ Server: "Here is my cert + OCSP response" โ
โ Client: verifies the stapled response locally โ
โ โ
โ Requires: ssl_stapling on, ssl_trusted_certificate, resolverโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
HTTP-to-HTTPS Redirect with ACME Exemption
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PORT 80 MUST SERVE THE ACME CHALLENGE, THEN REDIRECT โ
โ โ
โ server { โ
โ listen 80; โ
โ server_name example.com; โ
โ โ
โ location /.well-known/acme-challenge/ { โ
โ root /var/www/html; โ Serve challenge file โ
โ } โ
โ โ
โ location / { โ
โ return 301 https://$host$request_uri; โ
โ } โ
โ } โ
โ โ
โ Without the exemption, renewal fails because the redirect โ
โ prevents Let's Encrypt from fetching the challenge file. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| TLS module | ngx_http_ssl_module |
| Enable TLS | listen 443 ssl; |
| Certificate | ssl_certificate (full chain) |
| Private key | ssl_certificate_key |
| Protocol versions | ssl_protocols TLSv1.2 TLSv1.3; |
| Cipher suites | ssl_ciphers (TLS 1.2 only) |
| Session cache | ssl_session_cache shared:SSL:10m; |
| OCSP stapling | ssl_stapling on; + ssl_trusted_certificate |
| HSTS | add_header Strict-Transport-Security ... |
| Certificate tool | Certbot (certbot --nginx) |
| Renewal test | certbot renew --dry-run |
| HTTP redirect | return 301 https://$host$request_uri; |
Key takeaways:
- The certificate chain must be in the correct order.
ssl_certificatemust point to a file where the server certificate is first and the intermediates follow. The wrong order prevents nginx from starting . - TLS 1.2 and TLS 1.3 only. The
ssl_protocolsdirective should explicitly exclude TLS 1.0 and 1.1. Modern nginx defaults are secure, but the explicit directive makes the policy independent of the build . - OCSP stapling requires a resolver and a trusted certificate. The
ssl_stapling ondirective is not sufficient on its own; thessl_trusted_certificateandresolverdirectives are required for the stapling verification to function . - HSTS tells the browser to always use HTTPS. The
Strict-Transport-Securityheader withmax-ageandincludeSubDomainsenforces HTTPS for future requests. Thepreloadtoken should be omitted until the configuration is confirmed stable. - Certbot automates issuance and renewal. The
--nginxplugin modifies the server block and installs a systemd timer. The--dry-runflag tests renewal without consuming the production rate limits . - Port 80 must remain open. The ACME HTTP-01 challenge requires port 80, and renewal fails silently if the port is closed. The HTTP-to-HTTPS redirect must exempt the
/.well-known/acme-challenge/path .
Remember: TLS configuration is two layers. The certificate layer provides the identity and the chain. The policy layer controls the protocol versions, the cipher suites, and the security headers. The certificate comes from Certbot or another ACME client; the policy is written in the server block. Point ssl_certificate at the full chain, set ssl_protocols to TLS 1.2 and 1.3, enable OCSP stapling with the trusted certificate, and add the HSTS header. Keep port 80 open for renewal, and test with certbot renew --dry-run before relying on the automatic timer.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!