yum upgrades for production use, this is the repository for you.
Active subscription is required.
Configuring nginx tls 1.3 correctly is essential for achieving an A+ rating on SSL Labs while maximizing both security and performance. This comprehensive guide walks you through every step of hardening your NGINX TLS configuration, from basic setup to advanced optimizations.
Why TLS 1.3 Matters for Your NGINX Server
TLS 1.3 isn’t just an incremental update—it’s a fundamental improvement in how encrypted connections work. Here’s what makes it essential:
- Faster handshakes: TLS 1.3 reduces the handshake from two round-trips to just one, improving Time to First Byte (TTFB)
- Stronger security: Removed support for legacy algorithms like RSA key exchange, RC4, SHA-1, and CBC mode ciphers
- Forward secrecy by default: Every connection uses ephemeral keys, so compromising your private key doesn’t expose past traffic
- Simplified cipher suites: Only five secure cipher suites, eliminating configuration mistakes
Modern browsers have supported TLS 1.3 since 2018, and there’s no reason to use older protocols unless you need compatibility with ancient clients.
Prerequisites
Before configuring nginx tls 1.3 hardening, ensure your system meets these requirements:
- NGINX 1.13.0+ (for TLS 1.3 support; 1.20+ recommended)
- OpenSSL 1.1.1+ (the minimum version with TLS 1.3 support)
- A valid SSL certificate (from Let’s Encrypt or a commercial CA)
Check your versions:
nginx -v
openssl version
On RHEL 9, Rocky Linux 9, or AlmaLinux 9, the default packages meet these requirements:
dnf install nginx openssl
For the latest NGINX mainline (1.28.x) with all the newest features and optimizations, use the GetPageSpeed repository:
dnf install https://extras.getpagespeed.com/release-latest.rpm
dnf install nginx
Understanding Mozilla’s SSL Configurations
Mozilla maintains the definitive SSL Configuration Generator that provides three security profiles:
| Profile | TLS Versions | Use Case |
|---|---|---|
| Modern | TLS 1.3 only | Maximum security, modern clients only |
| Intermediate | TLS 1.2 + 1.3 | Balanced security and compatibility |
| Old | TLS 1.0+ | Legacy compatibility (avoid if possible) |
For most production servers, the Intermediate configuration provides the best balance. Use Modern only when you’re certain all your clients support TLS 1.3.
Step 1: Create the SSL Hardening Configuration
Create a dedicated configuration file for your TLS settings. This keeps your SSL configuration modular and easy to maintain.
Modern Configuration (TLS 1.3 Only)
For maximum security when you don’t need legacy client support:
# /etc/nginx/conf.d/ssl-hardening.conf
# Mozilla Modern Configuration - TLS 1.3 Only
ssl_protocols TLSv1.3;
ssl_ecdh_curve X25519:prime256v1:secp384r1;
ssl_prefer_server_ciphers off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 127.0.0.53 valid=300s;
resolver_timeout 5s;
With TLS 1.3, you don’t need to specify ssl_ciphers because the protocol only supports secure AEAD ciphers. The ssl_prefer_server_ciphers off directive is correct here—TLS 1.3 clients are trusted to choose appropriate ciphers.
Intermediate Configuration (TLS 1.2 + 1.3)
For broader compatibility while maintaining strong security:
# /etc/nginx/conf.d/ssl-hardening.conf
# Mozilla Intermediate Configuration - TLS 1.2 + 1.3
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ecdh_curve X25519:prime256v1:secp384r1;
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;
ssl_prefer_server_ciphers off;
# Session settings (required for TLS 1.2)
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# DH parameters for DHE ciphers
ssl_dhparam /etc/nginx/dhparam.pem;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 127.0.0.53 valid=300s;
resolver_timeout 5s;
Step 2: Generate DH Parameters (Intermediate Configuration Only)
If you’re using the Intermediate configuration with DHE ciphers, you need Diffie-Hellman parameters. Mozilla provides pre-generated, safe parameters:
curl https://ssl-config.mozilla.org/ffdhe2048.txt -o /etc/nginx/dhparam.pem
Using Mozilla’s pre-generated parameters is recommended over generating your own because:
- They’re cryptographically verified safe primes
- No risk of weak parameters from poor entropy during generation
- Consistent across deployments
If you prefer generating your own (takes several minutes):
openssl dhparam -out /etc/nginx/dhparam.pem 2048
Step 3: Configure Your Server Block
Apply the SSL configuration to your server block. The syntax differs depending on your NGINX version:
NGINX 1.20.x – 1.24.x (RHEL 9 default)
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
server_tokens off;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
root /var/www/example.com;
index index.html;
}
# HTTP to HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
NGINX 1.25.1+ (GetPageSpeed repo)
In newer NGINX versions, HTTP/2 is enabled with a separate directive:
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com www.example.com;
server_tokens off;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
root /var/www/example.com;
index index.html;
}
# HTTP to HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
Important Notes on the Configuration
The server_tokens off directive prevents NGINX from disclosing its version number in error pages and the Server response header. This is a basic security hardening measure that helps prevent host header injection vulnerabilities.
The ssl_trusted_certificate directive is required for OCSP stapling to work. It should point to the certificate chain file (intermediate certificates).
If you’re interested in the latest protocols, you can also enable HTTP/3 (QUIC) on NGINX for even better performance.
Step 4: Configure HSTS (HTTP Strict Transport Security)
HSTS tells browsers to only connect to your site over HTTPS, preventing downgrade attacks and SSL stripping:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
The parameters explained:
| Parameter | Value | Meaning |
|---|---|---|
max-age |
63072000 | Browser remembers HTTPS-only for 2 years |
includeSubDomains |
– | Applies to all subdomains |
preload |
– | Eligible for browser preload lists |
always |
– | Send header even on error responses |
Warning: Only add preload if you’re certain all subdomains support HTTPS, as removal from preload lists takes months.
Step 5: Configure OCSP Stapling
OCSP stapling improves performance by having your server fetch and cache certificate validity status, rather than forcing each client to query the CA:
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 127.0.0.53 valid=300s;
resolver_timeout 5s;
Resolver Security Considerations
The resolver directive specifies DNS servers for OCSP queries. Using external resolvers like 8.8.8.8 or 1.1.1.1 introduces a security risk—a DNS spoofing attack could poison the resolver cache.
Best practices for the resolver:
- Use a local resolver:
127.0.0.53(systemd-resolved) or127.0.0.1(local DNS cache) - If using external resolvers: Enable DNSSEC validation on your local resolver
- Cloud environments: Use the provider’s internal DNS (e.g., 169.254.169.253 for AWS)
Step 6: Test Your Configuration
Syntax Validation
Always test your NGINX configuration before reloading:
nginx -t
Apply the Configuration
systemctl reload nginx
Test with SSL Labs
The industry-standard test for SSL configuration is Qualys SSL Labs Server Test. Enter your domain and verify you receive an A+ rating.
Key metrics to check:
- Protocol Support: Only TLS 1.2 and 1.3 (or TLS 1.3 only for Modern)
- Key Exchange: ECDHE or DHE with strong parameters
- Cipher Strength: 128-bit or higher AEAD ciphers
- Certificate: Valid chain, strong signature algorithm
- HSTS: Enabled with long max-age
Test with OpenSSL
Verify TLS 1.3 is working from the command line:
openssl s_client -connect example.com:443 -tls1_3 < /dev/null 2>&1 | grep "Protocol"
Expected output:
Protocol : TLSv1.3
Step 7: Validate with Gixy
Gixy is a powerful NGINX configuration analyzer that detects security misconfigurations automatically. It checks for TLS issues, header problems, and many other security concerns.
Install Gixy on RHEL-based systems:
dnf install https://extras.getpagespeed.com/release-latest.rpm
dnf install gixy
Run the analysis:
gixy /etc/nginx/nginx.conf
Gixy will report issues like:
- Weak SSL/TLS protocols enabled
- Missing
server_tokens off - External DNS resolvers
- Missing security headers
- And many other security issues
Understanding the ssl_prefer_server_ciphers Warning
Gixy may report a MEDIUM severity warning about ssl_prefer_server_ciphers off. This warning can be safely ignored when using Mozilla’s Intermediate or Modern configurations. Here’s why:
The disagreement explained:
Traditional security advice (including SSL Labs’ best practices) recommends ssl_prefer_server_ciphers on to force the server to choose ciphers, preventing clients from negotiating weak options.
However, Mozilla’s reasoning is different: when all ciphers in your list are secure (as they are in Mozilla’s curated configurations), there’s no weak cipher for a client to choose. In this case, letting the client choose (off) allows them to optimize for their hardware—mobile devices without AES-NI may perform better with ChaCha20-Poly1305, while servers with hardware acceleration benefit from AES-GCM.
When to use each setting:
| Cipher Configuration | ssl_prefer_server_ciphers |
Reason |
|---|---|---|
| Mozilla Modern/Intermediate | off |
All ciphers are secure; let clients optimize for their hardware |
| Custom list with mixed ciphers | on |
Force server to choose strongest cipher |
| Legacy compatibility (weak ciphers) | on |
Essential to prevent weak cipher negotiation |
The Mozilla configurations include only secure ciphers, so ssl_prefer_server_ciphers off is the correct choice for optimal client performance without sacrificing security.
Common Mistakes and How to Avoid Them
Mistake 1: Missing ssl_trusted_certificate
Without this directive, OCSP stapling fails silently:
nginx: [warn] "ssl_stapling" ignored, issuer certificate not found
Always specify the certificate chain:
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
Mistake 2: Forgetting the always Parameter on HSTS
Without always, NGINX won’t send the HSTS header on error responses (4xx, 5xx), leaving a potential security gap:
# Wrong
add_header Strict-Transport-Security "max-age=63072000";
# Correct
add_header Strict-Transport-Security "max-age=63072000" always;
Mistake 3: Using Session Tickets Without Key Rotation
If you enable ssl_session_tickets on, you must implement key rotation, otherwise the session ticket key could be compromised:
# Either disable session tickets
ssl_session_tickets off;
# Or implement key rotation with ssl_session_ticket_key
ssl_session_ticket_key /etc/nginx/ticket.key;
For most deployments, simply disabling session tickets is the safest approach.
Mistake 4: Using External DNS Resolvers
External resolvers like 8.8.8.8 can be vulnerable to DNS spoofing. Always prefer local resolvers for OCSP queries.
Performance Considerations
TLS 1.3 Performance Benefits
TLS 1.3 inherently improves performance:
- 1-RTT handshakes: Standard connections complete in one round-trip
- 0-RTT resumption: Returning clients can send data immediately (with security tradeoffs)
Session Cache Sizing
The ssl_session_cache shared:SSL:10m directive allocates 10 MB of shared memory for the session cache. Each megabyte stores approximately 4,000 sessions:
# 10m = ~40,000 sessions
ssl_session_cache shared:SSL:10m;
# For high-traffic sites
ssl_session_cache shared:SSL:50m;
CPU Considerations
TLS 1.3’s preferred X25519 key exchange is faster than traditional ECDHE-P256, reducing CPU overhead. The cipher suite CHACHA20-POLY1305 is particularly efficient on servers without AES-NI hardware acceleration.
Complete Configuration Example
Here’s a production-ready complete configuration combining all elements:
# /etc/nginx/conf.d/ssl-hardening.conf
# Mozilla Intermediate Configuration for NGINX
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ecdh_curve X25519:prime256v1:secp384r1;
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;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_dhparam /etc/nginx/dhparam.pem;
ssl_stapling on;
ssl_stapling_verify on;
resolver 127.0.0.53 valid=300s;
resolver_timeout 5s;
# /etc/nginx/conf.d/example.com.conf
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
server_tokens off;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
Summary
Hardening nginx tls 1.3 requires attention to several interconnected settings:
- Use TLS 1.2+ at minimum, preferably TLS 1.3-only for modern deployments
- Follow Mozilla’s guidelines for cipher suites and protocol settings
- Enable OCSP stapling for performance and revocation checking
- Configure HSTS with a long max-age and the
alwaysparameter - Disable server_tokens to prevent information disclosure
- Use Gixy to automatically detect configuration issues
- Test with SSL Labs to verify your A+ rating
By following this guide, your NGINX server will have enterprise-grade TLS security that protects your users and your data.
