SSL Configuration Optimization #

Enabling HTTPS by installing an SSL/TLS certificate in Nginx is only the first step. By default, if we only write the ssl_certificate and ssl_certificate_key directives, Nginx will use built-in security parameters that may still allow vulnerable legacy protocols, use weak encryption algorithms (cipher suites), and not take advantage of performance optimization features. As a result, our server could be vulnerable to cyber attacks and have slow page load times due to repeated TLS handshake overhead.

To achieve the best security level (Grade A+ per SSL Labs scoring) while keeping fast access for users, we must do hardening and SSL/TLS configuration optimization in Nginx. In this article, we’ll learn the practical steps of choosing TLS protocol versions, designing strong cipher suites, configuring TLS Session Resumption, enabling OCSP Stapling for privacy and speed, implementing HTTP Strict Transport Security (HSTS), and preparing a production-ready configuration.

Restricting Protocols: TLSv1.2 and TLSv1.3 Only #

The first step in SSL/TLS hardening is disabling old protocols that have proven cryptographically insecure.

We must reject connections using:

  • SSLv2 and SSLv3: Obsolete and have fatal security flaws (like the POODLE attack).
  • TLSv1.0 and TLSv1.1: Officially deprecated by the IETF in 2021. Major browsers (Chrome, Firefox, Edge, Safari) no longer support these versions by default.

In Nginx, we restrict protocols with the ssl_protocols directive at the http or server block level:

# Restrict to modern protocols only
ssl_protocols TLSv1.2 TLSv1.3;

With this configuration, Nginx will reject the handshake if an old client tries to connect to our server using TLS 1.1 or below. This ensures all data exchanges are protected by modern protocols.


Configuring Strong and Secure Cipher Suites #

A Cipher Suite is a package of cryptographic algorithms that determines how a connection is secured. Each package contains an algorithm combination for:

  1. Key Exchange: e.g., ECDHE, DHE.
  2. Authentication: e.g., RSA, ECDSA.
  3. Encryption (Data Encryption): e.g., AES-GCM, ChaCha20-Poly1305.
  4. Integrity: e.g., SHA256, SHA384.

Mozilla provides an industry-standard cipher configuration guide that’s regularly updated. For most common web applications, we highly recommend the Intermediate profile because it offers very high security while maintaining compatibility with older browsers (including Android 5.0+ and legacy operating systems).

Here’s the ssl_ciphers directive based on the Mozilla Intermediate recommendation:

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;

# Disable forcing the server's cipher choice
ssl_prefer_server_ciphers off;

Why Do We Set ssl_prefer_server_ciphers off;? #

In the past, it was recommended to set this directive to on so the server forces its strongest cipher on the client. However, for modern web (especially since TLS 1.3), we’re advised to set it to off.

The reason: modern browsers (clients) know what hardware they’re using. For example, an older smartphone may not have hardware acceleration for AES encryption, but has acceleration for ChaCha20. By setting ssl_prefer_server_ciphers off, the server lets the client choose the cipher most optimal for their own hardware performance, as long as that cipher is in our server’s safe list.


Optimizing the Handshake with TLS Session Resumption #

The largest computational and network latency overhead in HTTPS happens during the initial asymmetric handshake. If users navigate between pages on our site or return within a short time, we don’t need to force them through a full handshake from scratch. We can resume the previous encrypted session using TLS Session Resumption.

There are two methods we can use:

1. TLS Session Cache (Primary Recommendation) #

This method stores session parameters in the Nginx server’s memory. Clients are identified using a unique Session ID during a shortened handshake.

# Create shared memory for all worker processes
# 10 Megabytes (10m) can hold about 40,000 active sessions
ssl_session_cache shared:SSL:10m;

# Session storage time limit in memory (1 day)
ssl_session_timeout 1d;

With the shared:SSL:10m parameter, all Nginx worker processes can access the same session cache. This guarantees that even if the user’s next request is handled by a different Nginx worker process, the HTTPS session can still resume quickly without a new handshake.

2. TLS Session Tickets (Forward Secrecy Risk) #

This method doesn’t store data on the server. Instead, the server encrypts session parameters and sends them back to the client as a “ticket”. On return visits, the client sends this ticket to the server to be decrypted and resume the session.

ssl_session_tickets off;

[!WARNING] Theoretically, session tickets are great for large server clusters because they don’t consume server memory. However, security-wise, if we don’t periodically rotate the ticket encryption key (ticket rotation) on the Nginx server, this can break Perfect Forward Secrecy (PFS). If the ticket key leaks in the future, an attacker who recorded past traffic could decrypt all sessions. Therefore, unless we have automatic ticket key rotation infrastructure, we highly recommend turning this feature off (ssl_session_tickets off;) and relying on ssl_session_cache, which is much safer.


OCSP Stapling: Improving Validation Privacy and Speed #

When a browser receives an SSL certificate from our server, the browser must make sure the certificate hasn’t been revoked by the CA before its validity expires (e.g., due to a private key leak).

There are two ways this verification happens:

1. Traditional Method (Without Stapling) #

The browser will pause page loading, then perform a DNS query and a separate HTTP connection to the CA’s OCSP server (e.g., to Let’s Encrypt’s server). This has two fatal drawbacks:

  • Speed: Adds connection latency (can take an extra 100ms - 500ms) before the web page starts rendering.
  • Privacy: The CA (like Let’s Encrypt or DigiCert) will know the user’s IP address and which sites the user is accessing in real-time.

2. OCSP Stapling Method (Best Solution) #

Nginx periodically (in the background) contacts the CA’s OCSP server, downloads a valid certificate validity proof digitally signed by the CA, and stores it in the server cache. When a user performs a TLS handshake with our server, Nginx attaches (“staples”) that validity proof directly inside the handshake packet. The browser can verify the certificate’s validity locally and instantly without needing to contact a third party.

flowchart TD
    subgraph "Traditional OCSP Query (Without Stapling)"
        A1["1. Browser Connects to Server"] --> A2["2. Server Sends Certificate"]
        A2 --> A3["3. Browser Pauses Page Load"]
        A3 --> A4["4. Browser Queries the CA Server on the Internet"]
        A4 --> A5["5. CA Server Answers: Valid"]
        A5 --> A6["6. Browser Continues Rendering the Page"]
    end

    subgraph "With OCSP Stapling (Fast & Private)"
        B1["1. Nginx Queries Periodically in the Background"] --> B2["2. CA Server Sends Signed Validity Proof"]
        B2 --> B3["3. Nginx Stores the Proof in Cache"]
        B3 --> B4["4. Browser Connects to Server"]
        B4 --> B5["5. Nginx Sends Certificate + Validity Proof Together"]
        B5 --> B6["6. Browser Verifies Locally & Renders the Page Immediately"]
    end

    classDef warn fill:#f59e0b,stroke:#d97706,color:#ffffff;
    classDef secure fill:#10b981,stroke:#059669,color:#ffffff;
    class A3,A4 warn;
    class B5,B6 secure;

OCSP Stapling Configuration in Nginx #

To enable this feature, add the following directives to our HTTPS server block:

# Enable OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;

# Intermediate CA certificate (needed for chain verification)
# If using Let's Encrypt, point to the chain.pem file
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

# Reliable DNS resolver for Nginx's background queries
# Use trusted DNS (Cloudflare & Google DNS) with a 300-second cache
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;

HSTS (HTTP Strict Transport Security): Browser-Side Security #

Even though we’ve already created an HTTP to HTTPS redirect in Nginx, an attacker can still perform a session hijacking attack at the start of the connection. For example, when a user types example.com in the browser, the browser by default sends a plain HTTP request (port 80) before being redirected to the HTTPS port (443) by our server. In that millisecond window, an attacker on public Wi-Fi can intercept the user’s connection (SSL Stripping attack).

The solution is the HSTS (HTTP Strict Transport Security) header. This header tells the browser that our domain may only be accessed over HTTPS. After the browser receives this header once, it automatically converts all http:// links to https:// locally before the request is even sent to the network.

In Nginx, we add this header using the add_header directive:

# Enable HSTS for 1 year (31536000 seconds)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
  • max-age=31536000: Tells the browser to remember this rule for 1 year. This time is refreshed every time the user revisits our site.
  • includeSubDomains: This rule also applies to all subdomains (e.g., api.example.com or blog.example.com).
  • always: Ensures this header is sent on all response types, including error responses (like 500 or 404).

[!CAUTION] Never enable HSTS before we’re truly sure that our entire site and all subdomains are ready to serve HTTPS connections stably. Once a user’s browser receives this header, the browser won’t be able to open our site via plain HTTP for the specified duration, and this rule can’t be revoked from the server side instantly.

HSTS Preload List #

If we want more extreme protection, we can register our domain on Google’s HSTS Preload List. Domains on this list are immediately marked HTTPS-only in the source code of the Chrome, Firefox, Safari, and Edge browsers from the moment the browser is installed. Browsers don’t need to visit our site first to receive the HSTS header.

To qualify for preload, add the preload parameter and change max-age to at least 2 years (63072000 seconds):

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

After installing the configuration above, register our domain at the official hstspreload.org site.


DH Parameters: Strengthening Custom Diffie-Hellman Keys #

For cipher suites that use the Ephemeral Diffie-Hellman (DHE) key exchange algorithm, Nginx by default uses DH parameters built into the OpenSSL library (usually 1024-bit on older Nginx versions). These weak built-in parameters are vulnerable to large-scale security exploits (like the Logjam attack).

We must create stronger custom DH parameters (2048-bit) using OpenSSL:

# Generate the dhparam.pem file (this process takes a few minutes)
sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048

After the file is successfully created, put its location into our Nginx configuration:

ssl_dhparam /etc/nginx/ssl/dhparam.pem;

Note: If our cipher suites only rely on the more modern ECDHE (Elliptic Curve Diffie-Hellman Ephemeral), these DH parameters won’t be actively used. However, providing them is excellent security hardening practice to ensure connection fallbacks stay secure.


Ready-to-Use Production SSL Configuration Template #

To keep our Nginx configuration tidy and not filled with repeated SSL code lines in every virtual host, we’re advised to create a separate SSL snippet file.

Step 1: Create the Snippet File /etc/nginx/snippets/ssl-params.conf #

# /etc/nginx/snippets/ssl-params.conf

# 1. Modern protocols
ssl_protocols TLSv1.2 TLSv1.3;

# 2. Chosen Cipher Suites (Mozilla Intermediate Profile)
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;

# 3. TLS Session Resumption
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

# 4. Diffie-Hellman strengthening
ssl_dhparam /etc/nginx/ssl/dhparam.pem;

# 5. OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;

# 6. HSTS Header (Remove preload if you're not sure yet)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# 7. Additional standard Security Headers
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;

Step 2: Use the Snippet in the Virtual Host File /etc/nginx/conf.d/example.com.conf #

Now, our server block configuration becomes very clean and easy to manage:

# HTTPS Server Block
server {
    listen 443 ssl;
    server_name example.com www.example.com;

    # SSL Certificate (e.g., from Let's Encrypt)
    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;

    # Include the SSL optimization snippet we created
    include snippets/ssl-params.conf;

    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

# HTTP Server Block (Redirect to HTTPS)
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

Run the test and reload Nginx to apply the configuration:

sudo nginx -t && sudo systemctl reload nginx

Testing the SSL Security Configuration Results #

After the configuration above is active, we must verify to make sure no security holes were missed.

1. Protocol Compliance Testing via CLI (OpenSSL) #

We can test locally whether our server really rejects obsolete protocols like TLS v1.1:

# Test the connection by forcing TLS 1.1 (Must FAIL / be rejected)
openssl s_client -connect localhost:443 -tls1_1

# Test the connection using TLS 1.3 (Must SUCCEED)
openssl s_client -connect localhost:443 -tls1_3

2. Comprehensive Testing via Web (SSL Labs) #

Test our web server’s security quality online using the free Qualys SSL Labs Server Test service.

Enter our public domain in the search box (e.g., example.com). The system will analyze our server for 1-3 minutes and provide a detailed report:

  • If we follow the modern protocol guide, Mozilla cipher suites, HSTS, and DH parameters above, our server is guaranteed to get Grade A+ (the highest score).
  • The system also checks whether our certificate is fully installed (full chain), vulnerability status to cryptographic exploits (like Heartbleed or Ticketbleed), and OCSP Stapling handshake performance.

Summary #

  • TLS 1.2 & 1.3: Restrict connections to only TLS 1.2 and TLS 1.3 protocols to avoid legacy security holes.
  • Shared Session Cache: Use ssl_session_cache shared:SSL:10m to share the session cache between Nginx worker processes to speed up repeated TLS handshakes.
  • Turn Off Session Tickets: Disable session tickets (ssl_session_tickets off;) unless we periodically rotate the ticket encryption keys, to preserve Perfect Forward Secrecy.
  • OCSP Stapling: Always enable OCSP Stapling so browsers don’t need to make slow queries to external CA servers, speeding up web page loading.
  • HSTS: Apply HSTS to lock browsers into only communicating over HTTPS, preventing SSL Stripping attacks.

← Previous: Let’s Encrypt   Next: HTTP/2 →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact