NGINX / Security

NGINX TLS 1.3 Hardening: A+ SSL Configuration Guide

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth 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) or 127.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:

  1. Use TLS 1.2+ at minimum, preferably TLS 1.3-only for modern deployments
  2. Follow Mozilla’s guidelines for cipher suites and protocol settings
  3. Enable OCSP stapling for performance and revocation checking
  4. Configure HSTS with a long max-age and the always parameter
  5. Disable server_tokens to prevent information disclosure
  6. Use Gixy to automatically detect configuration issues
  7. 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.

Further Reading

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.