InfraRunBook
    Back to articles

    Nginx SSL/TLS Hardening: Cipher Suites, TLS 1.3, OCSP Stapling, and Session Tuning

    Nginx
    Published: Mar 25, 2026
    Updated: Mar 25, 2026

    A complete production-grade guide to hardening Nginx TLS configuration with strong cipher suites, TLS 1.3, OCSP stapling, custom DH parameters, and session cache tuning for an A+ SSL Labs score.

    Nginx SSL/TLS Hardening: Cipher Suites, TLS 1.3, OCSP Stapling, and Session Tuning

    Introduction

    Obtaining a TLS certificate and enabling HTTPS on Nginx is only the beginning. A default or minimally configured TLS setup often supports deprecated protocol versions like TLS 1.0 and TLS 1.1, weak cipher suites, and missing features like OCSP stapling — all of which leave your server exposed to downgrade attacks, protocol exploits, and unnecessarily slow handshakes.

    This guide walks through a complete, production-grade SSL/TLS hardening process for Nginx on Ubuntu. You will disable legacy protocols, configure modern cipher suites, generate custom Diffie-Hellman parameters, enable OCSP stapling, and tune session resumption — finishing with a configuration that scores A+ on SSL Labs and passes a testssl.sh audit.

    All examples assume your domain is solvethenetwork.com, your server hostname is sw-infrarunbook-01, and your certificates are managed by Let's Encrypt. The TLS hardening directives apply equally to certificates from any certificate authority.

    Prerequisites

    • Nginx 1.18 or later installed on Ubuntu 22.04
    • A valid TLS certificate for solvethenetwork.com (e.g., from Let's Encrypt)
    • Root or sudo access on sw-infrarunbook-01
    • Port 443 open in your firewall
    • sw-infrarunbook-01 able to reach the internet for OCSP resolution

    Understanding the Default Nginx TLS Posture

    Out of the box, or after a basic Certbot installation, your Nginx server block likely contains something like this:

    ssl_certificate /etc/letsencrypt/live/solvethenetwork.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/solvethenetwork.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    The

    options-ssl-nginx.conf
    provided by Certbot is reasonable but does not represent the most aggressive hardening available. It may still permit cipher suites that are technically valid but suboptimal, and it does not configure OCSP stapling, session ticket behavior, or buffer size tuning. This guide replaces that include with a fully explicit, tuned configuration you control entirely.

    Step 1: Restrict TLS Protocol Versions

    TLS 1.0 and TLS 1.1 are deprecated by RFC 8996 and must never be offered on a production server. TLS 1.2 remains widely supported and is acceptable when combined with strong ciphers. TLS 1.3 is the preferred protocol and should always be enabled.

    Open your virtual host configuration on sw-infrarunbook-01:

    sudo nano /etc/nginx/sites-available/solvethenetwork.com

    Set the

    ssl_protocols
    directive to allow only TLS 1.2 and TLS 1.3:

    ssl_protocols TLSv1.2 TLSv1.3;

    This single directive eliminates SSLv3, TLS 1.0, and TLS 1.1 entirely. Any client that cannot negotiate TLS 1.2 or higher will receive a handshake failure. This is an acceptable trade-off given that all modern browsers have supported TLS 1.2 since at least 2013, and TLS 1.3 is supported in Chrome 70+, Firefox 63+, Safari 12.1+, and Edge 79+.

    Step 2: Configure Strong Cipher Suites

    Cipher suite selection determines the algorithms used for key exchange, authentication, bulk encryption, and message authentication. Weak ciphers enable attacks including SWEET32 (against 3DES), BEAST (against CBC in TLS 1.0), POODLE (against SSLv3), and ROBOT (against RSA key exchange). The hardened cipher list below restricts all TLS 1.2 connections to ECDHE-based key exchange (providing forward secrecy) with AES-GCM or ChaCha20-Poly1305 for bulk encryption.

    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;

    Note that

    ssl_prefer_server_ciphers off
    is intentional. TLS 1.3 manages its own cipher selection independently of this directive — setting it to
    off
    allows clients to choose their preferred cipher from the server's approved list, which matters for mobile clients that handle ChaCha20-Poly1305 more efficiently than AES-GCM in the absence of hardware acceleration. Since your cipher list is already restricted to strong algorithms only, allowing the client preference is safe and improves performance.

    For TLS 1.3, Nginx inherits cipher suites from the underlying OpenSSL library. The three TLS 1.3 cipher suites that OpenSSL supports —

    TLS_AES_128_GCM_SHA256
    ,
    TLS_AES_256_GCM_SHA384
    , and
    TLS_CHACHA20_POLY1305_SHA256
    — are all considered secure, so no additional restriction is needed.

    Step 3: Generate Custom Diffie-Hellman Parameters

    DHE key exchange requires a set of DH parameters. Default parameters on some distributions can be 1024-bit, which are considered weak. Generating your own 2048-bit DH parameters on sw-infrarunbook-01 is a hardening best practice and ensures the parameters were not shared with other installations.

    sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048

    This command takes one to two minutes to complete. The output file should be protected:

    sudo chmod 600 /etc/nginx/dhparam.pem
    sudo chown root:root /etc/nginx/dhparam.pem

    Reference the generated file in your Nginx configuration:

    ssl_dhparam /etc/nginx/dhparam.pem;

    NIST SP 800-131A considers 2048-bit DH parameters secure through at least 2030. If you are operating in a high-security environment, you can generate 4096-bit parameters, but this increases TLS handshake CPU cost — 2048-bit is the correct default for most production web servers.

    Step 4: Enable OCSP Stapling

    Online Certificate Status Protocol (OCSP) is how browsers verify that your certificate has not been revoked. Without OCSP stapling, each client browser must independently contact your certificate authority's OCSP responder during the TLS handshake. This adds latency, creates a privacy concern (the CA learns which sites users visit), and introduces a single point of failure if the CA's OCSP service is slow or unavailable.

    With OCSP stapling, Nginx periodically fetches the signed OCSP response from the CA and caches it in memory. During the TLS handshake, Nginx attaches (staples) this response, eliminating the client's need to contact the CA directly. The response is cryptographically signed by the CA, so clients can verify it without trusting Nginx.

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

    The

    ssl_trusted_certificate
    directive must point to the CA's certificate chain — for Let's Encrypt this is
    chain.pem
    , which contains the intermediate certificate. Do not use
    fullchain.pem
    here, as that file includes your end-entity certificate and will cause Nginx to fail OCSP response verification. The
    resolver
    directive specifies DNS servers that Nginx uses to resolve the OCSP responder hostname. If sw-infrarunbook-01 is behind a firewall with a local DNS resolver at 192.168.1.53, you can specify that instead, provided it can resolve public hostnames.

    Step 5: Configure SSL Session Cache and Disable Session Tickets

    Session resumption allows returning clients to reconnect without a full TLS handshake, reducing latency significantly. Nginx supports two mechanisms: server-side session cache and client-side session tickets. They serve the same purpose but have different security trade-offs.

    Session Cache (Recommended)

    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    The

    shared:SSL:10m
    value creates a 10 MB shared memory zone named SSL, accessible across all Nginx worker processes. This stores approximately 40,000 sessions simultaneously. The one-day timeout matches typical browser reconnect patterns for returning users.

    Session Tickets (Disabled)

    ssl_session_tickets off;

    Session tickets use a server-side encryption key to encode session state that is sent to and stored by the client. The client presents this ticket on reconnect, and the server decrypts it to resume the session. The problem is that if the ticket encryption key is compromised — or if it is never rotated — all past sessions encrypted with that key can be decrypted, violating forward secrecy. Unless you implement proper ticket key rotation with an external key management system, disabling session tickets is the correct production default.

    Step 6: Tune ssl_buffer_size for Lower Latency

    The default

    ssl_buffer_size
    in Nginx is 16 KB, which is optimized for bulk throughput. The server buffers up to 16 KB of response data before sending the first TLS record. For small API responses, small HTML pages, or JSON payloads, this inflates Time to First Byte (TTFB) unnecessarily.

    ssl_buffer_size 4k;

    Setting it to 4 KB means small responses are transmitted in fewer, earlier TLS records. For a file download server where large files dominate, 16 KB is correct. For a typical web application or API on solvethenetwork.com, 4 KB reduces perceived latency without meaningful throughput cost.

    Step 7: The Complete Hardened Server Block

    Assemble all directives into the complete production configuration for sw-infrarunbook-01. Edit

    /etc/nginx/sites-available/solvethenetwork.com
    :

    server {
        listen 80;
        server_name solvethenetwork.com www.solvethenetwork.com;
        return 301 https://$host$request_uri;
    }
    
    server {
        listen 443 ssl;
        http2 on;
        server_name solvethenetwork.com www.solvethenetwork.com;
    
        # Certificate paths
        ssl_certificate /etc/letsencrypt/live/solvethenetwork.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/solvethenetwork.com/privkey.pem;
    
        # Protocol restriction
        ssl_protocols TLSv1.2 TLSv1.3;
    
        # Cipher suites (TLS 1.2 explicit; TLS 1.3 managed by OpenSSL)
        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;
    
        # Custom DH parameters
        ssl_dhparam /etc/nginx/dhparam.pem;
    
        # OCSP stapling
        ssl_stapling on;
        ssl_stapling_verify on;
        ssl_trusted_certificate /etc/letsencrypt/live/solvethenetwork.com/chain.pem;
        resolver 1.1.1.1 8.8.8.8 valid=300s;
        resolver_timeout 5s;
    
        # Session resumption
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 1d;
        ssl_session_tickets off;
    
        # Buffer tuning
        ssl_buffer_size 4k;
    
        # 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 Referrer-Policy "strict-origin-when-cross-origin" always;
    
        root /var/www/solvethenetwork.com/html;
        index index.html;
    
        location / {
            try_files $uri $uri/ =404;
        }
    }

    Test and reload Nginx:

    sudo nginx -t && sudo systemctl reload nginx

    Step 8: Extract a Reusable ssl-params.conf Snippet

    When sw-infrarunbook-01 hosts multiple virtual hosts, duplicating TLS hardening directives in every server block creates configuration drift. Extract the shared directives into a snippet file:

    sudo nano /etc/nginx/snippets/ssl-params.conf
    # /etc/nginx/snippets/ssl-params.conf
    
    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:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_dhparam /etc/nginx/dhparam.pem;
    
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;
    
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    ssl_buffer_size 4k;
    
    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 Referrer-Policy "strict-origin-when-cross-origin" always;

    Each server block then only needs the certificate-specific lines plus the include:

    server {
        listen 443 ssl;
        http2 on;
        server_name solvethenetwork.com;
    
        ssl_certificate /etc/letsencrypt/live/solvethenetwork.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/solvethenetwork.com/privkey.pem;
        ssl_trusted_certificate /etc/letsencrypt/live/solvethenetwork.com/chain.pem;
    
        include snippets/ssl-params.conf;
    
        # ... application-specific directives
    }

    Step 9: Verifying the Hardened Configuration

    Test Protocol Restriction with OpenSSL

    From a separate host, confirm that legacy protocol versions are rejected by sw-infrarunbook-01:

    # Should fail with handshake error
    openssl s_client -connect solvethenetwork.com:443 -tls1
    
    # Should also fail
    openssl s_client -connect solvethenetwork.com:443 -tls1_1
    
    # Should succeed and show negotiated cipher
    openssl s_client -connect solvethenetwork.com:443 -tls1_2
    
    # Should succeed and show TLSv1.3 in output
    openssl s_client -connect solvethenetwork.com:443 -tls1_3

    Verify OCSP Stapling

    openssl s_client -connect solvethenetwork.com:443 -status 2>&1 | grep -A 10 'OCSP response'

    A working OCSP staple produces output containing

    OCSP Response Status: successful (0x0)
    . If you see
    no response sent
    immediately after a reload, wait 30 to 60 seconds — Nginx fetches the OCSP response asynchronously on the first post-reload request. If it never populates, check Nginx error logs:

    sudo tail -f /var/log/nginx/error.log | grep -i ocsp

    The most common error is

    ssl_stapling_verify: certificate not found
    , which means
    ssl_trusted_certificate
    is pointing to
    fullchain.pem
    instead of
    chain.pem
    .

    Run a Full Audit with testssl.sh

    Install testssl.sh on sw-infrarunbook-01 for a comprehensive local audit:

    git clone --depth 1 https://github.com/drwetter/testssl.sh.git /opt/testssl.sh
    /opt/testssl.sh/testssl.sh solvethenetwork.com:443

    After applying the configuration in this guide, the output should show:

    • SSLv2, SSLv3, TLS 1.0, TLS 1.1: not offered
    • TLS 1.2, TLS 1.3: offered
    • OCSP stapling: offered
    • Forward Secrecy: offered (all server ciphers)
    • BEAST, POODLE, SWEET32, ROBOT, CRIME: all not vulnerable

    Step 10: Log TLS Protocol and Cipher per Request

    Add

    $ssl_protocol
    and
    $ssl_cipher
    to your Nginx access log to monitor what clients are actually negotiating. Edit
    /etc/nginx/nginx.conf
    and add a custom log format inside the
    http
    block:

    log_format tls_detail '$remote_addr - $remote_user [$time_local] '
                          '"$request" $status $body_bytes_sent '
                          '"$http_referer" "$http_user_agent" '
                          'tls=$ssl_protocol cipher=$ssl_cipher';

    Then reference it in the server block:

    access_log /var/log/nginx/access.log tls_detail;

    This produces log lines like:

    192.168.10.45 - - [26/Mar/2026:09:14:33 +0000] "GET / HTTP/2.0" 200 4823 "-" "Mozilla/5.0" tls=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384

    You can then monitor for any remaining TLS 1.0 or 1.1 clients from legacy internal systems using a simple grep against the log file:

    sudo grep 'tls=TLSv1 ' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn

    Step 11: Certificate Expiry Monitoring

    A hardened TLS configuration is only as strong as the certificate behind it. Add a simple expiry check script on sw-infrarunbook-01:

    sudo nano /usr/local/bin/check-cert-expiry.sh
    #!/bin/bash
    DOMAIN="solvethenetwork.com"
    WARN_DAYS=30
    EXPIRY=$(echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN:443" 2>/dev/null \
        | openssl x509 -noout -enddate 2>/dev/null \
        | cut -d= -f2)
    EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
    NOW_EPOCH=$(date +%s)
    DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
    if [ "$DAYS_LEFT" -lt "$WARN_DAYS" ]; then
        echo "WARNING: Certificate for $DOMAIN expires in $DAYS_LEFT days"
        exit 1
    fi
    echo "OK: Certificate for $DOMAIN expires in $DAYS_LEFT days"
    exit 0
    sudo chmod +x /usr/local/bin/check-cert-expiry.sh

    Schedule it via cron to run daily and alert infrarunbook-admin:

    sudo crontab -e
    # Daily certificate expiry check at 08:00
    0 8 * * * /usr/local/bin/check-cert-expiry.sh | mail -s "Cert Expiry: solvethenetwork.com" infrarunbook-admin@solvethenetwork.com

    Frequently Asked Questions

    Q: Why is ssl_prefer_server_ciphers set to off instead of on?

    A: Setting

    ssl_prefer_server_ciphers on
    was a best practice when TLS 1.0 and 1.1 were still in use, because it forced the server's stronger cipher ordering over potentially weaker client preferences. For TLS 1.3, the directive has no effect — TLS 1.3 manages cipher selection independently. For TLS 1.2, since you are already restricting the cipher list to only strong ECDHE ciphers, the ordering matters less. Setting it to
    off
    allows clients to select their preferred cipher from your approved list, which improves performance on mobile clients that handle ChaCha20-Poly1305 more efficiently than AES-GCM in software.

    Q: Should I generate 4096-bit DH parameters instead of 2048-bit?

    A: NIST SP 800-131A considers 2048-bit DH parameters secure through 2030. 4096-bit parameters provide a larger security margin but significantly increase TLS handshake CPU cost, particularly on servers without hardware acceleration. For most production web servers, 2048-bit is the correct balance of security and performance. If you are operating in a high-assurance environment such as financial services, healthcare systems, or government infrastructure, 4096-bit is worth the trade-off.

    Q: My OCSP stapling shows no response in the OpenSSL output right after a reload. Is something broken?

    A: No, this is expected behavior. Nginx fetches the OCSP response asynchronously after the first HTTPS request following a reload. Until that asynchronous fetch completes and the response is cached, Nginx does not staple anything. Wait 30 to 60 seconds after your first HTTPS request, then run the OpenSSL status check again. If the response never appears after several minutes, check that your

    resolver
    directive is configured correctly and that sw-infrarunbook-01 can reach the CA's OCSP responder hostname over the internet on port 80.

    Q: Can I use ssl_session_tickets if I rotate the keys properly?

    A: Yes. Session tickets are safe when the ticket encryption key is rotated regularly — typically every 24 hours — and old keys are retired after a grace period to allow in-flight sessions to complete. OpenSSL supports configuring multiple ticket keys to enable graceful rotation. The practical challenge is that key rotation requires external tooling or custom scripting to manage key generation, distribution to all workers, and expiry. Because the default Nginx behavior does not rotate keys automatically, disabling session tickets is the safer and simpler default. For high-traffic deployments where session resumption performance is critical, implementing proper key rotation and enabling tickets is a valid choice.

    Q: Does restricting to TLS 1.2 and 1.3 break compatibility with older Android or iOS devices?

    A: Android 5.0 (2014) and later, and iOS 9 (2015) and later, both support TLS 1.2 with the cipher suites in this guide. Android 4.x and below will fail to connect, but these represent a negligible fraction of global traffic. If you have a documented requirement to support legacy Android 4.x devices, you can add

    ECDHE-RSA-AES128-SHA
    to your cipher list as a fallback. This is rarely appropriate for new deployments and weakens your overall cipher posture.

    Q: What is the difference between ssl_certificate and ssl_trusted_certificate?

    A:

    ssl_certificate
    specifies the certificate chain presented to clients during the TLS handshake. It should point to
    fullchain.pem
    , which contains your end-entity certificate plus any intermediate certificates required to chain up to the root CA.
    ssl_trusted_certificate
    is used exclusively for OCSP stapling response verification and should point to only the CA chain — for Let's Encrypt this is
    chain.pem
    , containing the intermediate certificate. Including your end-entity certificate in
    ssl_trusted_certificate
    causes OCSP verification to fail.

    Q: Is TLS 1.3 automatically enabled when I specify ssl_protocols TLSv1.2 TLSv1.3?

    A: Only if your underlying OpenSSL version supports TLS 1.3. OpenSSL 1.1.1 (shipped with Ubuntu 18.04+) and OpenSSL 3.x (Ubuntu 22.04+) both support TLS 1.3. Run

    openssl version
    on sw-infrarunbook-01 to confirm. If you are on an older distribution with OpenSSL 1.0.x, the TLSv1.3 entry in
    ssl_protocols
    will be silently ignored and only TLS 1.2 will be offered. Ubuntu 22.04 ships with OpenSSL 3.0.x, so TLS 1.3 is available by default.

    Q: Should I set ssl_ecdh_curve explicitly?

    A: Modern Nginx and OpenSSL automatically negotiate the best elliptic curve from a default list that includes X25519, P-256, and P-384 in priority order. You only need to set

    ssl_ecdh_curve
    explicitly if you want to restrict or reorder that list. For example,
    ssl_ecdh_curve X25519:P-256:P-384;
    deprioritizes P-384 which has a larger key size and slower operations on some hardware. The defaults in Nginx 1.18+ are already appropriate for production — explicit configuration is only needed if your security policy mandates specific curve restrictions.

    Q: What SSL Labs score does this configuration achieve?

    A: This configuration consistently achieves A+ on the Qualys SSL Labs server test. The contributing factors are: TLS 1.0 and 1.1 disabled (removes protocol score penalty), cipher suites restricted to forward-secret ECDHE families (eliminates cipher weakness flags), OCSP stapling enabled (positive trust indicator), HSTS header with a long max-age and includeSubDomains (required for A+), and no weak DH parameters. The A+ designation specifically requires an HSTS max-age of at least 180 days; the 63072000-second value (approximately 2 years) used in this guide satisfies that requirement.

    Q: Why does ssl_buffer_size affect Time to First Byte?

    A: Nginx writes TLS records using

    ssl_buffer_size
    as the maximum record size. With the default 16 KB, Nginx buffers up to 16 KB of response data before it sends the first TLS record across the wire. For a 3 KB API JSON response, Nginx will fill the 16 KB buffer partially before deciding to transmit, introducing unnecessary delay. Reducing
    ssl_buffer_size
    to 4 KB means small responses are packaged into fewer, earlier TLS records. The trade-off is slightly higher per-record overhead for large file transfers, which is why 16 KB is the default. For mixed workloads dominated by small page loads and API calls, 4 KB reduces perceived latency without meaningful impact on throughput.

    Q: Do I need to regenerate DH parameters when I renew my TLS certificate?

    A: No. DH parameters are independent of the certificate and its associated key pair. The certificate contains your public key used for authentication; the DH parameters are used for key exchange to establish the session encryption keys. Your DH parameters at

    /etc/nginx/dhparam.pem
    remain valid indefinitely and do not need to be regenerated on certificate renewal. You might choose to regenerate them periodically as part of a security hygiene rotation policy, but this is not required and is infrequently done in practice.

    Q: How do I verify which cipher was negotiated for a specific connection in Nginx logs?

    A: Add the

    $ssl_protocol
    and
    $ssl_cipher
    variables to a custom log format in
    /etc/nginx/nginx.conf
    as shown in Step 10 of this guide. Once the format is active, you can inspect the access log to see exactly which protocol version and cipher suite each client negotiated. This is particularly useful for identifying legacy internal clients that may be connecting with TLS 1.2 after you have deployed TLS 1.3, or for confirming that ChaCha20-Poly1305 is being selected by mobile clients as expected.

    Frequently Asked Questions

    Why is ssl_prefer_server_ciphers set to off instead of on?

    Setting ssl_prefer_server_ciphers on was a best practice when TLS 1.0 and 1.1 were still in use. For TLS 1.3, the directive has no effect — TLS 1.3 manages cipher selection independently. For TLS 1.2, since the cipher list is already restricted to only strong ECDHE ciphers, setting it to off allows clients to choose their preferred cipher from the approved list, improving performance on mobile clients that handle ChaCha20-Poly1305 more efficiently than AES-GCM in software.

    Should I generate 4096-bit DH parameters instead of 2048-bit?

    NIST SP 800-131A considers 2048-bit DH parameters secure through 2030. 4096-bit parameters provide a larger security margin but significantly increase TLS handshake CPU cost. For most production web servers, 2048-bit is the correct balance. For high-assurance environments such as financial services or government infrastructure, 4096-bit is worth the trade-off.

    My OCSP stapling shows no response in the OpenSSL output right after a reload. Is something broken?

    No, this is expected behavior. Nginx fetches the OCSP response asynchronously after the first HTTPS request following a reload. Until that fetch completes, Nginx does not staple anything. Wait 30 to 60 seconds after your first HTTPS request, then re-test. If the response never appears, check that your resolver directive is configured correctly and that the server can reach the CA's OCSP responder over the internet.

    Can I use ssl_session_tickets if I rotate the keys properly?

    Yes. Session tickets are safe when the encryption key is rotated regularly — typically every 24 hours — and old keys are retired after a grace period. The challenge is that key rotation requires external tooling or custom scripting. Because Nginx does not rotate ticket keys automatically, disabling session tickets is the safer and simpler default for most deployments.

    Does restricting to TLS 1.2 and 1.3 break compatibility with older Android or iOS devices?

    Android 5.0 (2014) and later, and iOS 9 (2015) and later, both support TLS 1.2 with the cipher suites in this guide. Android 4.x and below will fail to connect, but these represent a negligible fraction of global traffic. If you have a documented requirement to support legacy Android 4.x devices, you can add ECDHE-RSA-AES128-SHA to the cipher list as a fallback, though this is rarely appropriate for new deployments.

    What is the difference between ssl_certificate and ssl_trusted_certificate?

    ssl_certificate specifies the certificate chain presented to clients during the TLS handshake — it should be fullchain.pem, which includes your end-entity certificate plus intermediates. ssl_trusted_certificate is used exclusively for OCSP stapling verification and should point to only the CA chain — for Let's Encrypt this is chain.pem. Including your end-entity certificate in ssl_trusted_certificate causes OCSP verification to fail.

    Is TLS 1.3 automatically enabled when I specify ssl_protocols TLSv1.2 TLSv1.3?

    Only if your underlying OpenSSL version supports TLS 1.3. OpenSSL 1.1.1 (Ubuntu 18.04+) and OpenSSL 3.x (Ubuntu 22.04+) both support TLS 1.3. Run openssl version to confirm. On Ubuntu 22.04 with OpenSSL 3.0.x, TLS 1.3 is available by default.

    Should I set ssl_ecdh_curve explicitly?

    Modern Nginx and OpenSSL automatically negotiate the best elliptic curve from a default list including X25519, P-256, and P-384. You only need to set ssl_ecdh_curve explicitly if you want to restrict or reorder that list. The defaults in Nginx 1.18+ are already appropriate for production — explicit configuration is only needed if your security policy mandates specific curve restrictions.

    What SSL Labs score does this configuration achieve?

    This configuration consistently achieves A+ on the Qualys SSL Labs server test. The contributing factors are: TLS 1.0 and 1.1 disabled, cipher suites restricted to forward-secret ECDHE families, OCSP stapling enabled, and an HSTS header with max-age of at least 180 days. The 63072000-second value used in this guide (approximately 2 years) satisfies the A+ requirement.

    Why does ssl_buffer_size affect Time to First Byte?

    Nginx writes TLS records using ssl_buffer_size as the maximum record size. With the default 16 KB, Nginx buffers up to 16 KB of response data before sending the first TLS record. For a 3 KB API response this introduces unnecessary delay. Reducing ssl_buffer_size to 4 KB means small responses are transmitted sooner, reducing perceived latency for typical web and API traffic.

    Related Articles