Servers.

Building Nginx and OpenSSL from Source

The Project So Far

Getting Started: Log Analysis and TLS Versions Configuring Nginx for Security and Performance

This page explains how to build both the cryptographic toolkit OpenSSL and the web server Nginx from source code.

You shouldn't need to do this now, you should be able to proceed directly to configuring Nginx for an A+ Qualys score. That is, unless you're stuck using an old Linux distribution, such as RHEL/CentOS 7. If you are in that situation, then whatever requirement keeps you on an old Linux release might also prevent using software downloaded and built from source.

But, to help those people who do need to build their own OpenSSL or Nginx software, let's see how to do it!

What Do We Need?

Try Google Cloud Platform and receive $50

When I first 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

First, download the current version of Nginx.

OpenSSL 1.1.1 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! Download the current version of OpenSSL.

Verifying
Digital
Signatures

Download the compressed archive files, along with associated digital signatures and hashes.
nginx-x.y.z.tar.gz
nginx-x.y.z.tar.gz.asc
openssl-x.y.z.tar.gz
openssl-x.y.z.tar.gz.sha256

Verify digital signatures and hashes, and 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

Update: I will update the rest of this page from time to time with later versions of both OpenSSL and Nginx. I won't promise to update this page every time I update my software, but at least some of the time. Here are the SHA-2-256 hashes for later versions of the source code:

$ openssl sha256 *.tar
SHA256(nginx-1.21.1.tar)= 1e77fb7fd4aa36f8867b234cb57be7475dcab63e92fde1c4f1a487e375013d14
SHA256(openssl-1.1.1k.tar)= e931ce2c775214399b812559d688df5e6392a4a41d31a44334319d38757a7933

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-version and the new custom-built Nginx under /usr/local/nginx-version. They would create their own hierarchy of subdirectories, with bin, sbin, lib, and share having the same meanings. This way I could have multiple custom-built versions installed at the same time. I could test the very latest but still have the previous one(s) available as a fall-back. Then I could easily remove an older version I no longer need.

Build OpenSSL First

This is easy:

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

**********************************************************************
***                                                                ***
***   OpenSSL has been successfully configured                     ***
***                                                                ***
***   If you encounter a problem while building, please open an    ***
***   issue on GitHub <https://github.com/openssl/openssl/issues>  ***
***   and include the output from the following command:           ***
***                                                                ***
***       perl configdata.pm --dump                                ***
***                                                                ***
***   (If you are new to OpenSSL, you might want to consult the    ***
***   'Troubleshooting' section in the INSTALL file first)         ***
***                                                                ***
**********************************************************************

$ make
[... much output, 12-14 minutes of compiling ...]
$ su root -c 'make install'
[... asked for root password, output of installing almost 1,200 files ...]

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.1k/bin/openssl version
ld-elf.so.1: Shared object "libssl.so.1.1" not found, required by "openssl"
$ export LD_LIBRARY_PATH=/usr/local/openssl-1.1.1k/lib
$ /usr/local/openssl-1.1.1k/bin/openssl version
OpenSSL 1.1.1k  25 Mar 2021 

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.1k/share/man ciphers
[... manual page for custom-built version 1.1.1k ...] 

You might notice that version 1.1.1* supports fewer ciphers than 1.0.2*. This is because the newer 1.1.1* version is more secure! It refuses to use weaker ciphers like DES, 3DES, and RC4. Compare the results of these two commands:

$ openssl ciphers | egrep --color 'DES|RC4'
[... output including DES, 3DES, and RC4 variants ...]
$ /usr/local/openssl-1.1.1k/bin/openssl ciphers | egrep --color 'DES|RC4'
[... no output ...]

Now Build Nginx

Advanced topic: Consider using Poudriere to create and test these as FreeBSD packages.

Now we can build Nginx, telling it where to install itself, what to build, and where to find the OpenSSL code. Notice that you tell it where the OpenSSL source code is located, and not where the openssl program and libraries and manual pages were installed. Change /home/cromwell in the below to your home directory, or wherever you have the OpenSSL source code.

$ cd
$ tar xf nginx-1.21.1.tar.gz
$ cd nginx-1.21.1
$ ./configure --prefix=/usr/local/nginx-1.21.1	\
		--with-http_ssl_module			\
		--with-http_v2_module			\
		--with-http_geoip_module		\
		--with-http_gunzip_module		\
		--with-http_sub_module			\
		--with-openssl=/home/cromwell/openssl-1.1.1k
[... much output ...]

Configuration summary
  + using system PCRE library
  + using OpenSSL library: /home/cromwell/openssl-1.1.1k
  + using system zlib library

  nginx path prefix: "/usr/local/nginx-1.21.1"
  nginx binary file: "/usr/local/nginx-1.21.1/sbin/nginx"
  nginx modules path: "/usr/local/nginx-1.21.1/modules"
  nginx configuration prefix: "/usr/local/nginx-1.21.1/conf"
  nginx configuration file: "/usr/local/nginx-1.21.1/conf/nginx.conf"
  nginx pid file: "/usr/local/nginx-1.21.1/logs/nginx.pid"
  nginx error log file: "/usr/local/nginx-1.21.1/logs/error.log"
  nginx http access log file: "/usr/local/nginx-1.21.1/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, 14-18 minutes of compiling ...]
$ su root -c 'make install'
[... asked for root password, much output ...] 

Now I will make symbolic links so generic names point to the current versions.

$ su
# cd /usr/local
# rm -f openssl nginx
# ln -s openssl-1.1.1k openssl
# ln -s nginx-1.21.1 nginx 

And, to get the new version pointed to my configuration and content, change this as needed:

# cd /usr/local/nginx-1.21.1
# mv html html-original
# ln -s /home/cromwell/www html
# ln -s /home/cromwell/www/nginx/ssl_dhparam ssl_dhparam
# rm conf/nginx.conf
# ln -s /home/cromwell/www/nginx/nginx.conf conf/nginx.conf 

Make sure that the new software, your configuration file, and the symbolic links are all ready to go:

# /usr/local/nginx/sbin/nginx -t
nginx: the configuration file /usr/local/nginx-1.21.1/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx-1.21.1/conf/nginx.conf test is successful 

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 Old Tools Might Not Realize That

The problem is that our measurement tools may fail to show the TLS functionality. Your web browser, the (default) OpenSSL package, the Wireshark protocol analyzer, and online scanners should all be unable to test or recognize TLS 1.3. If not, you need to update your tools!

Make sure you can test your server from a distance:

$ 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)
---
QUIT 

Yes! It works!

I captured that traffic with Wireshark. On a recent distribution, Wireshark can figure it out.

Wireshark analyzing TLS 1.3 traffic

Also try testing it with the Qualys web server evaluation scanner. Their development scanner at dev.ssllabs.com may be able to do additional tests.

We Can Do Better

Tuning HTTPS
Security
on Apache

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:0.80rem;">
<?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.3 / TLS_AES_256_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.

Configuring the Server

Now you're ready for the last step:
Configuring Nginx for an A+ Qualys Score