Tightening the allowed TLS cipher set in Nginx reduces the chance of clients negotiating weak encryption and makes security posture consistent across virtual hosts. A curated cipher list also helps meet compliance requirements by removing legacy algorithms and key exchange modes from the negotiation path.

During the TLS handshake, the client and server agree on a protocol version (for example, TLSv1.2 or TLSv1.3) and a cipher suite that both sides support. Nginx passes cipher configuration to its underlying OpenSSL (or LibreSSL) library via directives like ssl_protocols, ssl_ciphers, and ssl_prefer_server_ciphers inside the http or server context.

Cipher tuning can break connectivity when legacy clients require older protocols or non-AEAD ciphers, and certificate type influences which cipher families can actually be used (RSA vs ECDSA). ssl_ciphers primarily affects TLSv1.2 and older, while TLSv1.3 uses ciphersuites handled differently by the crypto library. Keep console or out-of-band access available for recovery, and validate configuration with nginx -t before reloading.

Steps to configure TLS ciphers in Nginx:

  1. Locate existing ssl_protocols and ssl_ciphers directives in the active Nginx configuration.
    $ sudo nginx -T 2>/dev/null | grep -nE '^\s*(ssl_protocols|ssl_ciphers|ssl_prefer_server_ciphers|ssl_conf_command)\b|listen\s+443'
    64:    listen 443 ssl http2;
    71:    ssl_protocols TLSv1.2 TLSv1.3;
    72:    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
    ##### snipped #####
  2. Select a cipher policy baseline that matches client requirements.
    https://ssl-config.mozilla.org/

    Disabling TLSv1.0 and TLSv1.1 blocks legacy clients, including some older Java and embedded stacks.

  3. Create a global TLS policy file at /etc/nginx/conf.d/tls-ciphers.conf.
    $ sudo tee /etc/nginx/conf.d/tls-ciphers.conf >/dev/null <<'EOF'
    ssl_protocols TLSv1.2 TLSv1.3;
    
    ssl_prefer_server_ciphers on;
    
    ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    
    ssl_ecdh_curve X25519:secp256r1:secp384r1;
    EOF

    ssl_ciphers is an OpenSSL cipher string and mainly applies to TLSv1.2 and older, while TLSv1.3 ciphersuites are typically controlled by the crypto library defaults (or optional ssl_conf_command Ciphersuites support on newer builds).

    An overly strict cipher list can lock out required clients and cause immediate handshake failures on /443.

  4. Confirm the TLS policy file is parsed by Nginx in the http context.
    $ sudo nginx -T 2>/dev/null | grep -n 'configuration file /etc/nginx/conf.d/tls-ciphers.conf'
    189:# configuration file /etc/nginx/conf.d/tls-ciphers.conf:

    When the file is missing from nginx -T output, verify /etc/nginx/nginx.conf includes /etc/nginx/conf.d/*.conf inside the http block.

  5. Identify per-server overrides that conflict with the global cipher policy.
    $ sudo nginx -T 2>/dev/null | grep -n '^\s*ssl_ciphers\b'
    72:    ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    310:   ssl_ciphers 'HIGH:!aNULL:!MD5';
    ##### snipped #####

    server context directives override http context directives, so remove or align the older overrides to avoid unexpected negotiation.

  6. Validate the Nginx configuration before applying changes.
    $ sudo nginx -t
    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
    nginx: configuration file /etc/nginx/nginx.conf test is successful
  7. Reload nginx to apply the updated cipher policy.
    $ sudo systemctl reload nginx
  8. Confirm the nginx service remains active (running) after reload.
    $ sudo systemctl status nginx --no-pager
    ● nginx.service - A high performance web server and a reverse proxy server
         Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
         Active: active (running) since Sun 2025-12-14 10:02:11 UTC; 8s ago
    ##### snipped #####
  9. Verify a TLSv1.2 handshake negotiates an allowed cipher.
    $ openssl s_client -connect your.domain.tld:443 -servername your.domain.tld -tls1_2 -cipher 'ECDHE-RSA-AES128-GCM-SHA256' </dev/null
    CONNECTED(00000003)
    ##### snipped #####
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES128-GCM-SHA256
    Verify return code: 0 (ok)

    When the certificate is ECDSA-only, switch the test cipher to an ECDHE-ECDSA-* variant.

  10. Verify a TLSv1.3 handshake succeeds when enabled.
    $ openssl s_client -connect your.domain.tld:443 -servername your.domain.tld -tls1_3 -ciphersuites 'TLS_AES_128_GCM_SHA256' </dev/null
    CONNECTED(00000003)
    ##### snipped #####
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_128_GCM_SHA256
    Verify return code: 0 (ok)
  11. Verify deprecated protocol versions fail the handshake when disabled.
    $ openssl s_client -connect your.domain.tld:443 -servername your.domain.tld -tls1_1 </dev/null
    CONNECTED(00000003)
    ##### snipped #####
    no peer certificate available
    ##### snipped #####
Discuss the article:

Comment anonymously. Login not required.