Rotors of M-209 cipher machine.

Running TLS 1.3 with OpenSSL and Nginx

The Project So Far

Getting
Started

On the first page I showed how to log the TLS protocol and negotiated cipher, and then to analyze the log with basic Linux/UNIX command-line utilities.

I concluded that there was no pressing reason to keep supporting TLS 1.0. And, there were good reasons to add TLS 1.3. Security and performance would improve.

At the time that meant using recent versions of the OpenSSL cryptographic libraries and the Nginx web server. I needed newer versions than what was included with the operating system. So, they needed to be built from source.

Let's get started.

What Do We Need?

When I did this project in September, 2018, the Apache web server did not support TLS 1.3. It didn't matter if you had the needed shared libraries. Apache still thought that a directive to include TLS 1.3 was incorrect configuration syntax. From the Apache release notes:

This release is compatible with OpenSSL versions from 0.9.8a to 1.1.0 only, and does not support TLSv1.3. Future releases of httpd 2.4 are expected to add compatibility with OpenSSL 1.1.1 and enable support for TLSv1.3.

There were some patches that could make it happen, but that would make ongoing maintenance too much of a hassle.

Nginx was the clear choice of web server. It uses the OpenSSL cryptographic libraries to do TLS.

FreeBSD on Google Compute Platform

I was doing this project on FreeBSD, where OpenSSL is included in the base operating system and Nginx is a standard package. However, FreeBSD 11.2-RELEASE included earlier versions: OpenSSL 1.0.2o and Nginx 1.14.0. (That version of Nginx supported TLS 1.3, but it was compiled to use the standard system OpenSSL libraries, which I didn't want to replace.) At the time, Mageia Linux had OpenSSL 1.0.2p, and Mint Linux had OpenSSL 1.1.0g.

The FreeBSD Ports system provides a way of getting more or less the same things. I needed to build Nginx and OpenSSL from source.

Getting The Software

The current version of Nginx was 1.15.4.

The current version of OpenSSL was 1.1.1. It had been released on September 11th, just 3 to 4 weeks after RFC 8446 was published, formally defining TLS 1.3. There had been a series of 28 drafts of that document. Some earlier versions of OpenSSL support Draft #28, which ended up being the final definition. But let's start with the official versions!

Verifying
Digital
Signatures

I downloaded the compressed archive files, along with associated digital signatures and hashes.
nginx-1.15.4.tar.gz
nginx-1.15.4.tar.gz.asc
openssl-1.1.1.tar.gz
openssl-1.1.1.tar.gz.sha256
Verify digital signatures and hashes, make sure you're getting the real software from the original organization.

Here's what I got for the SHA-2-256 hashes of the uncompressed archives:

$ openssl sha256 *.tar
SHA256(nginx-1.15.4.tar)= 50363df790ebddf94c6c99574cd24e5a455a8e72473317d8c5fa115abf3cbde6
SHA256(openssl-1.1.1.tar)= b735c3eda1230dcdffa1f2651c677c7b661ad2798199847734ee337aea6749e9

Building The Software

Where should the software be installed? I have to decide that before I start building it. The standard locations are:

I didn't want to use any of those! I would put the new custom-built OpenSSL under /usr/local/openssl-1.1.1 (as I could see maybe having more than one custom version at some point!) and the new custom-built Nginx under /usr/local/nginx. They would create their own hierarchy of subdirectories, with bin, sbin, lib, and share having the same meanings.

Build OpenSSL First

This is easy:

$ tar xf openssl-1.1.1.tar.gz
$ cd openssl-1.1.1
$ ./config --prefix=/usr/local/openssl-1.1.1 \
		--openssldir=/usr/local/openssl-1.1.1
Operating system: amd64-whatever-freebsd
Configuring OpenSSL version 1.1.1 (0x1010100fL) for BSD-x86_64
Using os-specific seed configuration
Creating configdata.pm
Creating Makefile

**********************************************************************
***                                                                ***
***   If you want to report a building issue, please include the   ***
***   output from this command:                                    ***
***                                                                ***
***     perl configdata.pm --dump                                  ***
***                                                                ***
**********************************************************************

$ make
[... much output, 12-14 minutes of compiling ...]
# su root -c 'make install'
[... asked for root password, much output ...]

Once it's installed, you have to set the LD_LIBRARY_PATH environment variable before you can use the new binary.

$ /usr/local/openssl-1.1.1/bin/openssl version
Shared object "libssl.so.1.1" not found, required by "openssl"
$ export LD_LIBRARY_PATH=/usr/local/openssl-1.1.1/lib
$ /usr/local/openssl-1.1.1/bin/openssl version
OpenSSL 1.1.1  11 Sep 2018 

You can similarly ask for the manual pages from the new custom version.

$ man ciphers
[... manual page for software included with the OS ...]
$ man -M /usr/local/openssl-1.1.1/share/man ciphers
[... manual page for custom-built version 1.1.1 ...] 

Now Build Nginx

Now we can build Nginx, telling it where to install itself, what to build, and where to find the OpenSSL code.

$ cd
$ tar xf nginx-1.15.4.tar.gz
$ cd nginx-1.15.4
$ ./configure --prefix=/usr/local/nginx	\
		--with-http_ssl_module		\
		--with-http_v2_module		\
		--with-http_geoip_module	\
		--with-http_gunzip_module	\
		--with-http_auth_request_module	\
		--with-openssl=/usr/src/openssl-1.1.1
[... much output ...]

Configuration summary
  + using system PCRE library
  + using OpenSSL library: /usr/src/openssl-1.1.1
  + using system zlib library

  nginx path prefix: "/usr/local/nginx"
  nginx binary file: "/usr/local/nginx/sbin/nginx"
  nginx modules path: "/usr/local/nginx/modules"
  nginx configuration prefix: "/usr/local/nginx/conf"
  nginx configuration file: "/usr/local/nginx/conf/nginx.conf"
  nginx pid file: "/usr/local/nginx/logs/nginx.pid"
  nginx error log file: "/usr/local/nginx/logs/error.log"
  nginx http access log file: "/usr/local/nginx/logs/access.log"
  nginx http client request body temporary files: "client_body_temp"
  nginx http proxy temporary files: "proxy_temp"
  nginx http fastcgi temporary files: "fastcgi_temp"
  nginx http uwsgi temporary files: "uwsgi_temp"
  nginx http scgi temporary files: "scgi_temp"

$ make
[... much output, 18-20 minutes of compiling ...]
# su root -c 'make install'
[... asked for root password, much output ...] 

I need to stop the running Apache process, and comment out the line in /etc/rc.conf that starts it at boot time.

$ grep apache /etc/rc.conf
## apache24_enable=YES 

I will create or modify /etc/rc.local to start nginx at boot time.

$ cat /etc/rc.local
#!/bin/sh

# Created just to start Nginx
/usr/local/nginx/sbin/nginx 

Initial Configuration

Dual ECC/RSA
Certificates

The web server should be ready to go once we define a simple configuration. Here are the contents of a basic nginx.conf file. This uses dual ECC and RSA keys and certificates. See my earlier page for details on how to generate and install these, and automatically renew them.

# Nginx configuration

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    index  Index.html;

    # HTTP server
    server {
	listen       80;
	server_name  cromwell-intl.com;
	# gzip suggestions from:
	# https://www.digitalocean.com/community/tutorials/how-to-increase-pagespeed-score-by-changing-your-nginx-configuration-on-ubuntu-16-04
	gzip on;
	gzip_comp_level 5;
	gzip_min_length 256;
	gzip_vary on;
	gzip_types text/plain text/css application/javascript application/x-javascript text/javascript image/x-icon application/x-font-ttf;

	return 301 https://$host$request_uri;

        location / {
	    root   html;
	}

	error_page 404 /ssi/404page.html;

    }

    # HTTPS server
    server {
	listen       443 ssl http2;
	server_name  cromwell-intl.com;
	keepalive_timeout 65;
	# gzip suggestions from:
	# https://www.digitalocean.com/community/tutorials/how-to-increase-pagespeed-score-by-changing-your-nginx-configuration-on-ubuntu-16-04
	gzip on;
	gzip_comp_level 5;
	gzip_min_length 256;
	gzip_vary on;
	gzip_types text/plain text/css application/javascript application/x-javascript text/javascript image/x-icon application/x-font-ttf;

	location / {
	    root   html;
	}

	error_page 404 /ssi/404page.html;

	####################################################################
	# Certificates and private keys.
	####################################################################

	# ECC
	ssl_certificate /usr/local/etc/letsencrypt/ecc-live/cromwell-intl.com/fullchain.pem;
	ssl_certificate_key /usr/local/etc/letsencrypt/ecc-live/cromwell-intl.com/privkey.pem;
	# RSA
	ssl_certificate /usr/local/etc/letsencrypt/rsa-live/cromwell-intl.com/fullchain.pem;
	ssl_certificate_key /usr/local/etc/letsencrypt/rsa-live/cromwell-intl.com/privkey.pem;

	####################################################################
	## Cryptography and TLS
	####################################################################

	ssl_protocols TLSv1.2 TLSv1.3;

	# SSL session cache timeout defaults to 5 minutes, 1 minute should
	# be plenty.  This is abused by advertisers like Google and Facebook,
	# long timeouts like theirs will look suspicious.  See, for example:
	# https://www.zdnet.com/article/advertisers-can-track-users-across-the-internet-via-tls-session-resumption/
	ssl_session_cache    shared:SSL:1m;
	ssl_session_timeout  5m;

	ssl_ciphers EECDH:ECDHE:EDH;

	ssl_prefer_server_ciphers on;

    }

} 

Start it, and make sure it's running on TCP ports 80 and 443. You will see a master process running as root. There should be a worker process running as nginx and listening on TCP/80 and TCP/443, highlighted here. Then lsof will also show the worker caught in the act of serving content.

# lsof -i tcp:80 -o -i tcp:443
COMMAND     PID   USER   FD   TYPE             DEVICE OFFSET NODE NAME
nginx     11117   root    9u  IPv4 0xfffff80024dde820    0t0  TCP *:http (LISTEN)
nginx     11117   root   10u  IPv4 0xfffff80024e4a410    0t0  TCP *:https (LISTEN)
nginx     11118 nobody    3u  IPv4 0xfffff80024d9e820    0t0  TCP www.c.cromwell-intl.internal:https->74-216-249-130.dedicated.allstream.net:54781 (ESTABLISHED)
nginx     11118 nobody    9u  IPv4 0xfffff80024dde820    0t0  TCP *:http (LISTEN)
nginx     11118 nobody   10u  IPv4 0xfffff80024e4a410    0t0  TCP *:https (LISTEN)
nginx     11118 nobody   13u  IPv4 0xfffff80024900820    0t0  TCP www.c.cromwell-intl.internal:https->ec2-54-172-123-82.compute-1.amazonaws.com:37726 (ESTABLISHED)
nginx     11118 nobody   14u  IPv4 0xfffff80024d98820    0t0  TCP www.c.cromwell-intl.internal:https->66.87.176.179:25208 (ESTABLISHED)
nginx     11118 nobody   15u  IPv4 0xfffff80024b22820    0t0  TCP www.c.cromwell-intl.internal:https->d24-36-29-150.home1.cgocable.net:55150 (ESTABLISHED)
nginx     11118 nobody   16u  IPv4 0xfffff80024b20410    0t0  TCP www.c.cromwell-intl.internal:https->71-80-191-91.static.lsan.ca.charter.com:62074 (ESTABLISHED)
nginx     11118 nobody   17u  IPv4 0xfffff80024e18820    0t0  TCP www.c.cromwell-intl.internal:https->ip72-207-111-102.sd.sd.cox.net:56285 (ESTABLISHED)
nginx     11118 nobody   18u  IPv4 0xfffff80024d9e000    0t0  TCP www.c.cromwell-intl.internal:http->180.115.150.200.static.copel.net:44473 (ESTABLISHED)
nginx     11118 nobody   19u  IPv4 0xfffff80024384410    0t0  TCP www.c.cromwell-intl.internal:https->8.30.181.58:54251 (ESTABLISHED)
nginx     11118 nobody   20u  IPv4 0xfffff80024f1a000    0t0  TCP www.c.cromwell-intl.internal:https->mail.iph-bet.fr:49700 (ESTABLISHED)
nginx     11118 nobody   22u  IPv4 0xfffff80024d9d410    0t0  TCP www.c.cromwell-intl.internal:https->180.115.150.200.static.copel.net:54777 (ESTABLISHED)
nginx     11118 nobody   23u  IPv4 0xfffff800247e4820    0t0  TCP www.c.cromwell-intl.internal:https->CPE84948c4cc1b3-CM84948c4cc1b0.cpe.net.cable.rogers.com:54286 (ESTABLISHED)
nginx     11118 nobody   24u  IPv4 0xfffff80024d98000    0t0  TCP www.c.cromwell-intl.internal:https->CPE84948c4cc1b3-CM84948c4cc1b0.cpe.net.cable.rogers.com:54287 (ESTABLISHED)
nginx     11118 nobody   25u  IPv4 0xfffff800247f5820    0t0  TCP www.c.cromwell-intl.internal:https->CPE84948c4cc1b3-CM84948c4cc1b0.cpe.net.cable.rogers.com:54289 (ESTABLISHED)
nginx     11118 nobody   26u  IPv4 0xfffff8002490e410    0t0  TCP www.c.cromwell-intl.internal:https->CPE84948c4cc1b3-CM84948c4cc1b0.cpe.net.cable.rogers.com:54288 (ESTABLISHED)
nginx     11118 nobody   27u  IPv4 0xfffff80024dde410    0t0  TCP www.c.cromwell-intl.internal:https->172.56.42.132:61366 (ESTABLISHED)
nginx     11118 nobody   28u  IPv4 0xfffff80024384820    0t0  TCP www.c.cromwell-intl.internal:https->190.106.223.59:45182 (ESTABLISHED)
nginx     11118 nobody   29u  IPv4 0xfffff80024d99000    0t0  TCP www.c.cromwell-intl.internal:https->dynamicip-94-181-133-82.pppoe.penza.ertelecom.ru:55386 (ESTABLISHED)
nginx     11118 nobody   56u  IPv4 0xfffff80024e4a000    0t0  TCP www.c.cromwell-intl.internal:https->66.87.176.179:30154 (ESTABLISHED)
python2.7 12908   root    7u  IPv4 0xfffff80024d9f000    0t0  TCP www.c.cromwell-intl.internal:55720->metadata.google.internal:http (ESTABLISHED)

It Should Be Running, But We May Not Realize That

The problem is that our measurement tools will very likely fail to show the TLS functionality. Your web browser, the (default) OpenSSL package, the Wireshark protocol analyzer, and online scanners like Qualys may all be unable to really test or recognize TLS 1.3.

I compiled a copy of OpenSSL 1.1.1 on my desktop. I didn't install it. I used the LD_LIBRARY_PATH trick to use it to test my server from a distance:

$ export LD_LIBRARY_PATH=/tmp/openssl-1.1.1/lib
$ /tmp/openssl-1.1.1/bin/openssl s_client -tls1_3 cromwell-intl.com:443
CONNECTED(00000005)
---
Certificate chain
 0 s:CN = cromwell-intl.com
   i:C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
 1 s:C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
   i:O = Digital Signature Trust Co., CN = DST Root CA X3
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIFdzCCBF+gAwIBAgISAzBCb9tcMGcrVV2eqVLf7J39MA0GCSqGSIb3DQEBCwUA
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
[... lines deleted ...]
sboGITUCwIdEGAPawfevVlv0xKG2g0oZK3dqhTXNIOuq3euzHf+NA+iUXVrO6ll7
Vy/rjUpz225dyEBtPULkwrVvN5bpNKutKgH8HE++mII47+sv3ef+jRs6b3/95m5f
Oz7WyhK2c3R0mGo=
-----END CERTIFICATE-----
subject=CN = cromwell-intl.com

issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3

---
No client certificate CA names sent
Peer signing digest: SHA384
Peer signature type: ECDSA
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 2986 bytes and written 321 bytes
Verification error: unable to get local issuer certificate
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 384 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 20 (unable to get local issuer certificate)
---
^C 

Yes! It works!

I captured that traffic with Wireshark. On a desktop with a year-old distribution, patched but locked in to older versions, Wireshark 2.2.17 incorrectly labels the traffic as TLS v1.2 and doesn't know what to make of the latest cipher specs. On a recent distribution, Wireshark 2.4.5 can figure it out.

Wireshark analyzing TLS 1.3 traffic

Also try testing it with the Qualys web server evaluation scanner. However, the default one at www.ssllabs.com could not do TLS 1.3 when I did the test. Apparently it used some earlier draft definition of TLS 1.3, and you would see a failed test for version 1.3. Use their development scanner at dev.ssllabs.com instead.

We Can Do Better

Tuning
HTTPS
Security

In an earlier series of pages I describe how to configure an Apache web server to score an A+ grade from Qualys and from online HTTP header analyzers. Here's how to do the same thing with Nginx.

I pass two variables to the PHP back end processor, which allows me to put the TLS protocol version and cipher in the standard footer on each page. The code in the footer looks like this:

<p style="text-align:right; font-size:80%;">
<?php	/* The trick here is to pass these values to the PHP processor
	 * in the Nginx configuration file:
	 *	fastcgi_param  TLS_PROTOCOL $ssl_protocol;
	 *	fastcgi_param  TLS_CIPHER $ssl_cipher;
	 */
	echo('Protocol: ' . $_SERVER["SERVER_PROTOCOL"] . '<br />');
	echo('Crypto: ' . $_SERVER["TLS_PROTOCOL"] . ' / ' . $_SERVER["TLS_CIPHER"] ); ?>
</p>

The result, specific to your connection to view this page, is:

Protocol: HTTP/1.1
Crypto: TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384

Maintaining Dual ECC and RSA Certificates

You need at least one key pair, with the public key wrapped in a digital certification issued by a trusted CA (or Certificate Authority). Another page of mine shows how to generate and maintain dual ECC and RSA certificates from the Let's Encrypt project, automatically renewing them.

Now, on to the Nginx configuration file. The comments should describe what's going on.

# Nginx configuration

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    index  Index.html;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
			'$status $body_bytes_sent "$http_referer" '
			'$ssl_protocol $ssl_cipher';

    access_log  /var/www/logs/httpd-access.log  main;
    error_log   /var/www/logs/httpd-error.log;

    # HTTP Server
    server {
	listen       80;
	server_name  cromwell-intl.com;
	gzip on;

	return 301 https://$host$request_uri;

	####################################################################
	## Logging
	####################################################################

	# Don't log images, CSS, Javascript, font files
	location ~* \.(js|css|png|jpg|jpeg|gif|ico|js|ttf)$ {
		access_log off;
	}

        location / {
	    root   html;
	}

	error_page 404 /ssi/404page.html;

	# redirect server error pages to the static page /50x.html
	#
	error_page   500 502 503 504  /50x.html;
	location = /50x.html {
	    root   html;
	}

	####################################################################
	# Process all *.html as PHP.  The php-fam service must be
	# running, listening on TCP/9000 on localhost only.
	# The try_files line serves the 404 error page if they ask for
	# a non-existant file.  Without that, you get a cryptic error.
	####################################################################
	location ~ \.html$ {
	    try_files      $uri =404;
	    fastcgi_pass   127.0.0.1:9000;
	    fastcgi_index  Index.html;
	    # Changed this line: original had: /scripts$fastcgi_script_name;
	    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
	    include        fastcgi_params;
	}

    }


    # HTTPS server
    server {
	listen       443 ssl http2;
	server_name  cromwell-intl.com;
	keepalive_timeout 65;

	####################################################################
	# gzip suggestions from:
	# https://www.digitalocean.com/community/tutorials/how-to-increase-pagespeed-score-by-changing-your-nginx-configuration-on-ubuntu-16-04
	####################################################################
	gzip on;
	gzip_comp_level 5;
	gzip_min_length 256;
	gzip_vary on;
	gzip_types text/plain text/css application/javascript application/x-javascript text/javascript image/x-icon application/x-font-ttf;

	####################################################################
	## Tell client to cache bulky data for 7 days, which is
	## the Google Pagespeed recommendation / requirement:
	## JavaScript, CSS, fonts, and images
	####################################################################
	location ~* \.(js|css|ttf|png|jpg|jpeg|gif|ico)$ {
		expires 7d;
		add_header Cache-Control "public, no-transform";
	}

	####################################################################
	## Logging
	####################################################################

	# Don't log images, CSS, Javascript, font files
	location ~* \.(js|css|png|jpg|jpeg|gif|ico|js|ttf)$ {
		access_log off;
	}

	# TCP Tuning
	#
	# Nagle's algorithm (potentially) adds a 0.2 second delay to
	# every TCP connection.  It made sense in the days of remote
	# keyboard interaction, but it gets in the way of transferring
	# many files.  Turn on tcp_nodelay to disable Nagle's algorithm.
	#
	# FreeBSD man page for tcp(4) says:
	#    TCP_NODELAY   Under most circumstances, TCP sends data when it is
	#                  presented; when outstanding data has not yet been
	#                  acknowledged, it gathers small amounts of output to
	#                  be sent in a single packet once an acknowledgement
	#                  is received.  For a small number of clients, such
	#                  as window systems that send a stream of mouse
	#                  events which receive no replies, this packetization
	#                  may cause significant delays.  The boolean option
	#                  TCP_NODELAY defeats this algorithm.
	#
	# It's on by default, but why not make it explicit:
	tcp_nodelay on;

	# tcp_nopush blocks data until either it's done or the packet
	# reaches the MSS, so you more efficiently stream data in
	# larger segments.  You can send a response header and the
	# beginning of a file in one packet, and generally send a
	# file with full packets.
	#
	# FreeBSD man page for tcp(4) says:
	#    TCP_NOPUSH    By convention, the sender-TCP will set the "push"
	#                  bit, and begin transmission immediately (if
	#                  permitted) at the end of every user call to
	#                  write(2) or writev(2).  When this option is set to
	#                  a non-zero value, TCP will delay sending any data
	#                  at all until either the socket is closed, or the
	#                  internal send buffer is filled.
	#
	# This is like the TCP_CORK socket option on Linux.  It's only
	# effective when sendfile is used.
	#
	tcp_nopush on;
	sendfile on;

	# html is either the real document root, or a
	# symbolic link to where the web site resides.
	location / {
	    root   html;
	}

	underscores_in_headers on;

	error_page 404 /ssi/404page.html;

	####################################################################
	# Certificates and private keys.
	# This will send both ECC and RSA certificates.
	####################################################################

	# ECC
	ssl_certificate /usr/local/etc/letsencrypt/ecc-live/cromwell-intl.com/fullchain.pem;
	ssl_certificate_key /usr/local/etc/letsencrypt/ecc-live/cromwell-intl.com/privkey.pem;
	# RSA
	ssl_certificate /usr/local/etc/letsencrypt/rsa-live/cromwell-intl.com/fullchain.pem;
	ssl_certificate_key /usr/local/etc/letsencrypt/rsa-live/cromwell-intl.com/privkey.pem;

	####################################################################
	## Cryptography and TLS
	####################################################################

	# TLS versions 1.2 and 1.3 only
	ssl_protocols TLSv1.2 TLSv1.3;

	# SSL session cache timeout defaults to 5 minutes, 1 minute should
	# be plenty.  This is abused by advertisers like Google and Facebook,
	# long timeouts like theirs will look suspicious.  See, for example:
	# https://www.zdnet.com/article/advertisers-can-track-users-across-the-internet-via-tls-session-resumption/
	ssl_session_cache    shared:SSL:1m;
	ssl_session_timeout  5m;

	## Specify the ciphers.  Guidance available here:
	##   https://mozilla.github.io/server-side-tls/ssl-config-generator/
	##   https://wiki.mozilla.org/Security/Server_Side_TLS
	##
	## Default, no specified ssl_ciphers line, or specifying HIGH,
	## yields 16 weak ciphers using RSA key exchange.
	##
	## See "man ciphers" under OpenSSL 1.1.1 for a mapping between
	## OpenSSL cipher names and IETF names.
	##
	## A simple solution is the following.  That includes a large
	## number of unneeded CBC mode ciphers considered less strong
	## but not (yet) dangerously weak:
	##   ssl_ciphers EECDH:ECDHE:EDH;
	##
	## That yields all 3 TLS 1.3 ciphers, then for TLS 1.2:
	##	14 scored strong,
	##	12 scored acceptable,
	##	 9 scored strong,
	##	 9 scored acceptable.
	## The only acceptable one needed for moderately old Safari is
	## TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,
	## which OpenSSL calls ECDHE-ECDSA-AES256-SHA384.
	##
	## The following is the result of doing that, looking at Qualys scan
	## results, and mapping the IETF names to OpenSSL names for just
	## the strong plus the one needed acceptable, and ordered a
	## little differently.  Then explicitly listing them.
	##
	## My preference, admittedly incompletely informed:
	##   First by major categories:
	##     ChaCha20-Poly1305
	##     TLS_ECDHE_ECDSA-*
	##     TLS_ECDHE_RSA-*
	##   Then, within those, 256-bit before 128-bit.
	##   Then, within those, AES before ARIA.
	##   Then, within those, GCM before CCM.
	##   Then, within those, CCM before CCM-8
	##	(CCM uses 128-bit tags, CCM-8 uses 64-bit tags,
	##	so, limited security against forgery, see:
	##	https://crypto.stackexchange.com/questions/50364/security-of-ccm-in-tls-in-comparison-to-gcm-sha-or-sha2-for-the-digest )
	##
	## Get choices and IETF names with:
	##      openssl -s -v
	##
	## HOWEVER:  OpenSSL's API does not let you specify the TLS 1.3
	## ciphers in the same way as the earlier ones.  TLS 1.3 ciphers
	## are set by:
	##    SSL_CTX_set_ciphersuites() and SSL_set_ciphersuites()
	## while TLS 1.2 and earlier are set by
	##    SSL_CTX_set_ciphers() and SSL_set_ciphers()
	## So, at least with OpenSSL 1.1.1, the Nginx (and Apache) weren't
	## initially sure if the API would be stable over the long run.
	## They don't use the *ciphersuites() functions, so you can't change
	## cipher preference order for TLS 1.3.  You get the default:
	##   TLS_AES_256_GCM_SHA384
	##   TLS_CHACHA20_POLY1305_SHA256
	##   TLS_AES_128_GCM_SHA256
	## See https://wiki.openssl.org/index.php/TLS1.3 for background.
	## Also see:
	##   https://github.com/ssllabs/ssllabs-scan/issues/636
	##   https://trac.nginx.org/nginx/ticket/1529
	##
	## Result:
	##      TLSv1.3 ciphers in default order
	##
	##	ECDHE-ECDSA-CHACHA20-POLY1305
	##	ECDHE-RSA-CHACHA20-POLY1305
	##	DHE-RSA-CHACHA20-POLY1305
	##	TLS_CHACHA20_POLY1305_SHA256
	##
	##	ECDHE-ECDSA-AES256-GCM-SHA384
	##	ECDHE-ECDSA-AES256-CCM
	##	ECDHE-ECDSA-AES256-CCM8
	##	ECDHE-ECDSA-ARIA256-GCM-SHA384
	##
	##	ECDHE-ECDSA-AES128-GCM-SHA256
	##	ECDHE-ECDSA-AES128-CCM
	##	ECDHE-ECDSA-AES128-CCM8
	##	ECDHE-ECDSA-ARIA128-GCM-SHA256
	##
	##	ECDHE-RSA-AES256-GCM-SHA384
	##	ECDHE-ARIA256-GCM-SHA384
	##	ECDHE-RSA-AES128-GCM-SHA256
	##	ECDHE-ARIA128-GCM-SHA256
	##
	##	DHE-RSA-AES256-GCM-SHA384
	##	DHE-RSA-AES256-CCM
	##	DHE-RSA-AES256-CCM8
	##	DHE-RSA-ARIA256-GCM-SHA384
	##
	##	DHE-RSA-AES128-GCM-SHA256
	##	DHE-RSA-AES128-CCM
	##	DHE-RSA-AES128-CCM8
	##	DHE-RSA-ARIA128-GCM-SHA256
	##
	##	Plus, for somewhat old Safari, Safari 6 / iOS 6.0.1
	##	through Safari 8 / OX X 10.10:
	##	ECDHE-ECDSA-AES256-SHA384
	##
	##	Unfortunately, unlike Apache, this
	##	has to be one enormously long line.
	##
	ssl_ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-CHACHA20-POLY1305:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-CCM:ECDHE-ECDSA-AES256-CCM8:ECDHE-ECDSA-ARIA256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-CCM:ECDHE-ECDSA-AES128-CCM8:ECDHE-ECDSA-ARIA128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ARIA256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ARIA128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-CCM:DHE-RSA-AES256-CCM8:DHE-RSA-ARIA256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-CCM:DHE-RSA-AES128-CCM8:DHE-RSA-ARIA128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384;

	# After all that work, have the server prefer them in that order.
	ssl_prefer_server_ciphers on;

	# Ed25519 curve first, NIST curves later.  There is
	# still speculation about NSA/NIST backdoors.
	ssl_ecdh_curve X25519:secp521r1:secp384r1;

	# This file contains the predefined DH group ffdhe4096 recommended
	# by IETF in RFC 7919.  Those are audited, may be more resistant
	# to attacks than randomly generated ones.  See:
	# https://wiki.mozilla.org/Security/Server_Side_TLS
	ssl_dhparam /usr/local/nginx/ssl_dhparam;

	####################################################################
	## Security headers
	####################################################################

	# HSTS
	add_header Strict-Transport-Security max-age=15768000;
	# OCSP Stapling
	ssl_stapling on;
	ssl_stapling_verify on;
	# X-Frame-Options
	add_header X-Frame-Options "SAMEORIGIN";
	# Turn on XSS / Cross-Site Scripting protection in browsers.
	# "1" = on,
	# "mode=block" = block an attack, don't try to sanitize it
	add_header X-Xss-Protection "1; mode=block";
	# Tell the browser (Chrome and Explorer, anyway) not to "sniff" the
	# content and try to figure it out, but simply use the MIME type
	# reported by the server.
	# This means that all files "*.jpg" must be JPEG, and so on!
	add_header X-Content-Type-Options "nosniff";
	# I set Referrer-Policy liberally.  I think referrer info
	# can be helpful without being absolutely trustworthy,
	# and I don't have any scandalous or sensitive URLs.
	# See https://scotthelme.co.uk/a-new-security-header-referrer-policy/
	add_header Referrer-Policy "no-referrer-when-downgrade";
	# Content Security Policy
	add_header Content-Security-Policy "style-src https://cromwell-intl.com https://*.googleapis.com 'unsafe-inline';";
	# Feature Policy, see:
	# https://scotthelme.co.uk/a-new-security-header-feature-policy/
	add_header Feature-Policy "usermedia 'none'";

	# Include the TLS protocol version and negotiated cipher
	# in the HTTP headers.
	add_header X-HTTPS-Protocol $ssl_protocol;
	add_header X-HTTPS-Cipher $ssl_cipher;

	####################################################################
	# Process all *.html as PHP.  The php-fam service must be
	# running, listening on TCP/9000 on localhost only.
	# The try_files line serves the 404 error page if they ask for
	# a non-existant file.  Without that, you get a cryptic error.
	####################################################################
	location ~ \.html$ {
##            root           html;
	    try_files      $uri =404;
	    fastcgi_pass   127.0.0.1:9000;
	    fastcgi_index  Index.html;
	    # Changed this line: original had: /scripts$fastcgi_script_name;
	    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
	    fastcgi_param  TLS_PROTOCOL $ssl_protocol;
	    fastcgi_param  TLS_CIPHER $ssl_cipher;
	    include        fastcgi_params;
	}

	####################################################################
	# .htaccess is specific to Apache.  Re-implement rules here.
	####################################################################
	[... many rewrite rules here ...]

    }

} 

You could also test with nmap, but remember that it will also depend on support from OpenSSL.

$ nmap --script ssl-enum-ciphers -p 443 cromwell-intl.com
[... scan output ...]
$ nmap --script-help=ssl-enum-ciphers
[... explanation ...]

There is much more that you can accomplish with Nginx. See the Nginx documentation for the details.