How to configure TLS ciphers in Nginx

Tightening the TLS cipher policy in Nginx removes weak negotiation choices, keeps HTTPS virtual hosts aligned with the same security baseline, and helps avoid scanner findings that come from old protocol or cipher settings lingering in one server block.

For current Nginx builds, ssl_ciphers sets the allowed cipher string for TLSv1.2 and older handshakes, while ssl_conf_command Ciphersuites … can pin the TLSv1.3 suites passed to the linked OpenSSL library. The negotiated result still depends on the protocol version, the client offer, and whether the server certificate uses RSA or ECDSA.

Examples below use packaged Nginx on Debian or Ubuntu, where the main file is /etc/nginx/nginx.conf and site blocks commonly live under /etc/nginx/sites-available/. Upstream Nginx now defaults to TLSv1.2 and TLSv1.3, but packaged configs may already define broader ssl_protocols or ssl_prefer_server_ciphers lines in the http context, so adding the same directive again in the same context can fail with a duplicate-directive error.

Steps to configure TLS ciphers in Nginx:

  1. Inspect the loaded HTTPS configuration and locate any existing TLS directives before editing.
    $ sudo nginx -T 2>&1 | grep -nE '^\s*(ssl_protocols|ssl_prefer_server_ciphers|ssl_ciphers|ssl_conf_command)\b|listen\s+443|server_name\s+host\.example\.net|configuration file '
    1:# configuration file /etc/nginx/nginx.conf:
    36:    ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
    37:    ssl_prefer_server_ciphers on;
    189:# configuration file /etc/nginx/sites-enabled/host.example.net.conf:
    191:    listen 443 ssl;
    192:    server_name host.example.net;

    Do not add a second ssl_protocols or ssl_prefer_server_ciphers line in the same http or server block. Update the existing directive or keep the policy at one configuration level only.

  2. List the TLSv1.2 cipher names supported by the local OpenSSL library so the configured string uses valid suite names.
    $ openssl ciphers -s -v -tls1_2 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'
    ECDHE-ECDSA-AES256-GCM-SHA384  TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESGCM(256)            Mac=AEAD
    ECDHE-RSA-AES256-GCM-SHA384    TLSv1.2 Kx=ECDH     Au=RSA   Enc=AESGCM(256)            Mac=AEAD
    ECDHE-ECDSA-CHACHA20-POLY1305  TLSv1.2 Kx=ECDH     Au=ECDSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
    ECDHE-RSA-CHACHA20-POLY1305    TLSv1.2 Kx=ECDH     Au=RSA   Enc=CHACHA20/POLY1305(256) Mac=AEAD
    ECDHE-ECDSA-AES128-GCM-SHA256  TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESGCM(128)            Mac=AEAD
    ECDHE-RSA-AES128-GCM-SHA256    TLSv1.2 Kx=ECDH     Au=RSA   Enc=AESGCM(128)            Mac=AEAD

    ssl_ciphers applies to TLSv1.2 and older. TLSv1.3 uses a separate ciphersuite list.

    The negotiated TLSv1.2 suite must match the certificate type in use. An RSA certificate will negotiate the ECDHE-RSA-… entries, not the ECDHE-ECDSA-… entries.

  3. Open the active HTTPS server block file.
    $ sudoedit /etc/nginx/sites-available/host.example.net.conf

    When the same TLS policy must apply to every HTTPS virtual host, place the directives once in the http context inside /etc/nginx/nginx.conf instead of repeating them in multiple server blocks.

  4. Set or update the protocol and cipher policy inside the active server block.
    server {
        listen 443 ssl;
        server_name host.example.net;
    
        ssl_certificate /etc/nginx/ssl/host.example.net.crt;
        ssl_certificate_key /etc/nginx/ssl/host.example.net.key;
    
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_prefer_server_ciphers on;
        ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
        ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256;
    }

    ssl_prefer_server_ciphers mainly affects pre-TLSv1.3 negotiation. On current builds, ssl_conf_command Ciphersuites … is the Nginx directive that can pin the TLSv1.3 suite list passed to OpenSSL.

    The current default for ssl_ecdh_curve is auto, so an explicit curve list is usually unnecessary unless policy or certificate curve requirements say otherwise.

    If nginx -t reports an unknown ssl_conf_command or rejects the Ciphersuites command, remove that line and keep the library default TLSv1.3 suites instead of forcing an unsupported OpenSSL path.

  5. Test the updated configuration before reloading Nginx.
    $ sudo nginx -t
    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
    nginx: configuration file /etc/nginx/nginx.conf test is successful
  6. Reload Nginx so new connections use the updated cipher policy.
    $ sudo systemctl reload nginx

    Use sudo nginx -s reload on systems where systemd is not managing the service.

  7. Confirm the service stayed active after the reload.
    $ sudo systemctl is-active nginx
    active

    If the unit reports anything other than active, inspect sudo journalctl --unit=nginx.service --no-pager --lines=20 before retrying the reload.

  8. Confirm that the loaded server block now contains the intended TLS directives.
    $ sudo nginx -T 2>&1 | sed -n '/host.example.net.conf:/,/ssl_conf_command/p'
    # configuration file /etc/nginx/sites-enabled/host.example.net.conf:
    server {
        listen 443 ssl;
        server_name host.example.net;
    
        ssl_certificate /etc/nginx/ssl/host.example.net.crt;
        ssl_certificate_key /etc/nginx/ssl/host.example.net.key;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_prefer_server_ciphers on;
        ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
        ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256;

    nginx -T is useful when the active configuration is assembled through several include directives and symlinked site files.

  9. Confirm a TLSv1.2 handshake negotiates one of the configured TLSv1.2 suites.
    $ echo | openssl s_client -connect host.example.net:443 -servername host.example.net -tls1_2 2>/dev/null | sed -n '/New, /p;/Protocol  :/p;/Cipher    :/p'
    New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384
        Protocol  : TLSv1.2
        Cipher    : ECDHE-RSA-AES256-GCM-SHA384

    With an RSA certificate, seeing an ECDHE-RSA-… suite is expected even when ECDHE-ECDSA-… appears earlier in the configured list.

  10. Confirm a TLSv1.3 handshake succeeds with an allowed TLSv1.3 ciphersuite.
    $ echo | openssl s_client -connect host.example.net:443 -servername host.example.net -tls1_3 -ciphersuites 'TLS_AES_256_GCM_SHA384' 2>/dev/null | sed -n '/New, /p'
    New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

    TLSv1.3 ciphersuites use the standard TLS_AES_* and TLS_CHACHA20_* names exposed by the linked OpenSSL library.

  11. Confirm disabled legacy protocols no longer complete the handshake.
    $ echo | openssl s_client -connect host.example.net:443 -servername host.example.net -tls1_1 2>&1 | sed -n '/no peer certificate available/p'
    no peer certificate available

    If a TLSv1.0 or TLSv1.1 test still succeeds, another server block, another reverse proxy, or another TLS terminator is likely answering the request instead of the updated Nginx block.