1 06. SSL
Asif Bacchus edited this page 2021-01-16 06:23:50 -07:00
This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

SSL (TLS… actually)

First off, for some reason when we talk about web servers we say SSL across the board when we really mean TLS… anyways, Ill continue that weird tradition… Setting up SSL on NGINX is not inherently difficult but it does have to be done properly or youll get errors thrown all over the place along with very unclear log messages. Fortunately, thats all been taken care of in this container and the recommended SSL configurations as suggested by Mozilla (using their SSL configuration tool) are preloaded. All you need to do is provide the actual certificates 😉.

Contents

Required files Mounting and enabling TLS mode HSTS TLS in custom server-blocks Redirect HTTP to HTTPS with your own server-blocks Lets Encrypt endpoint

Required files

file type container-location
Full-chain certificate
(certificate concatenated with intermediates and root CA)
/certs/fullchain.pem
Private key /certs/privkey.pem
Certificate chain
(intermediates concatenated with root CA or just root CA if no intermediates)
/certs/chain.pem
DH Parameters file
(NOT required for TLS 1.3-only mode)
/certs/dhparam.pem

Mounting and enabling

If you are using the default server-blocks, the container will detect mounted certificate files in the /certs directory and will auto-enable HTTPS by default and HTTP to HTTPS redirects. Assuming you have gathered all your certificate files in the ~/nginx/certs directory, you can mount them easily and the container will do the rest:

docker run -d --name ab-nginx --restart unless-stopped \
  -v /var/www:/usr/share/nginx/html \
  -v ~/nginx/certs:/certs:ro \
  asifbacchus/ab-nginx

TLS mode

By default the container runs in TLS 1.2 + 1.3 mode. If you want to enforce (i.e. only permit) TLS 1.3 mode, set TLS13_ONLY=TRUE like this:

docker run -d --name ab-nginx --restart unless-stopped \
  -v /var/www:/usr/share/nginx/html \
  -v ~/nginx/certs:/certs:ro \
  -e TLS13_ONLY=TRUE \
  asifbacchus/ab-nginx

You should note that in TLS 1.3-only mode NGINX will only accept TLS 1.3 connections it will not downgrade to TLS 1.2. That is the major difference between the modes. In the default mode, TLS 1.2 is set as the minimum requirement but TLS 1.3 will be used if both client and server agree. TLS 1.3-only mode simply sets TLS 1.3 as the minimum therefore TLS 1.2 is no longer an option.

HSTS

HSTS is a header that instructs clients they should only accept SSL/TLS connections from a server for a defined period of time. The server sets this period of time to the accepted minimum value of 6 months. In other words, clients are instructed to refuse non-SSL connections from the server for 6 months from the last time they received this header. That is why you should be careful when enabling this if you need to disable SSL during testing or whatever, but you have passed this header to clients already, they rightly will refuse HTTP connections!

All that being said, once you have sorted out your HTTPS settings and they are working, you absolutely should enable HSTS for security reasons and you should always run this container with this option enabled in production. To do so, set HSTS=TRUE:

# TLS 1.2+1.3 with HSTS
docker run -d --name ab-nginx --restart unless-stopped \
  -v /var/www:/usr/share/nginx/html \
  -v ~/nginx/certs:/certs:ro \
  -e HSTS=TRUE \
  asifbacchus/ab-nginx

# TLS 1.3-only with HSTS
docker run -d --name ab-nginx --restart unless-stopped \
  -v /var/www:/usr/share/nginx/html \
  -v ~/nginx/certs:/certs:ro \
  -e TLS13_ONLY=TRUE \
  -e HSTS=TRUE \
  asifbacchus/ab-nginx

TLS in custom server-blocks

If you are defining your own server-blocks, you only have to enable SSL on the listen directive. All other SSL configuration variables and certificate locations will be inherited from the default configuration. In other words, start your server-block like this:

server {
  listen 443 ssl http2;
  ...
}

You do not have define any other SSL options like ssl_protocols, ssl_ciphers, ssl_certificate, ssl_certificate_key, etc.

Redirect HTTP to HTTPS with your own server-blocks

The container constructs a file at /etc/nginx/server_names.conf based on the SERVER_NAMES environment variable at runtime. Using this file, its easy to do a global redirect for all names that the server answers. Create a server-block file called something like 01-redirectHTTPS.conf with the following configuration:

server {
  listen 80;
  include /etc/nginx/server_names.conf;
  
  # redirect all requests to HTTPS with the same hostname
  # change 443 if you are running a non-standard HTTPS port
  location / {
    return 301 https://$host:443$request_uri;
  }
  
  # optional: use the built-in fun error pages
  include /etc/nginx/errorpages.conf;
}

Lets Encrypt endpoint

The default server blocks are pre-set to answer Lets Encrypt queries and verify HTTP challenges. The only thing you need to do is mount a directory to /usr/share/nginx/letsencrypt in the container for the challenge authentication files to be written. Remember that UID 8080 must have write access to this directory! Once thats done, you can run certbot in CERTONLY mode so that it does not try to change any NGINX configurations.

This assumes you are running certbot on the host machine, which is what Id recommend instead of trying to cobble-together a purely docker solution. While its possible using docker-compose, I find it more reliable and resource-intelligent to run certbot on the host and then mount the certificates to the container.

If you need help getting certbot installed on your host and want an overview of what well be doing, check out my blog post here. I assume your host is Debian/Ubuntu in that post, but even if it isnt youll still get a good overview of how it all works. Check out certbots homepage if you need help installing on a different distro. From this point on, Ill assume certbot is installed and working.

Before we get started, we need to create a post-renew script that will copy our certs to the right place (in my experience more reliable than changing permissions on /etc/letsencrypt) and automatically restart our container after renewal. Something like this should work nicely:

#!/bin/sh

#
# Let's Encrypt post renewal script for ab-nginx
#

# copy certificates and fix permissions
cp /etc/letsencrypt/live/cert_name/*.pem /certs
chown root:8080 /certs/*
chmod 640 /certs/privkey.pem

# restart docker container to read new certs
docker restart ab-nginx

exit 0

A few notes:

  • choose a name for your certificate(s) this will make management much easier in the future especially if you end up dealing with multiple sets of certificates.
  • replace cert_name with the name you chose.
  • replace /certs with the name of the directory on your host you will be mounting to the container.

Go ahead and save this script somewhere that makes sense for executables, Im partial to/usr/local/bin/post-renew.sh. Now, lets make the directories we need and make this script executable. Alter your paths accordingly:

# make script executable
chmod +x /usr/local/bin/post-renew.sh

# make directory to copy our certs, fix permissions
mkdir /certs
chown root:8080 /certs
chmod 750 /certs

Ok, now we need to create another directory that we can bind to our container for the Lets Encrypt challenges. This directory doesnt hold anything important so it doesnt really matter where you put it. Then well spin up our container just long enough to answer challenges and get our certificates saved on the host.

# make a directory that we can bind to the container for LE challenges
mkdir /var/letsencrypt

# update permissions
chown 8080:8080 /var/letsencrypt
chmod 750 /var/letsencrypt

# run container temporarily
docker run -d --rm --name ab-nginx \
  -p 80:80 \
  -v /var/letsencrypt:/usr/share/nginx/letsencrypt \
  asifbacchus/ab-nginx

# get certificates
certbot certonly --webroot -w /var/letsencrypt --rsa-key 4096 -d example.com,www.example.com --cert-name cert_name --deploy-hook "/usr/local/bin/post-renew.sh" --agree-tos -m your@email.address --no-eff-email

# stop temporary container (will auto-remove itself because of the '--rm' flag)
docker stop ab-nginx

A few notes about the certbot command:

  • remember to replace example.com,www.example.com with the actual domain name(s) you need!
  • also remember that domains are processed in order, so the common name will be the first one you enter order matters!
  • replace cert_name with the certificate name you chose earlier.
  • deploy-hook will take care of running our post-renewal script each time the certificate is renewed.

Once that completes, our certificates should be issued and saved. Final set up before testing things out.

# copy certificates to the directory we will bind to the container and fix permissions
cp /etc/letsencrypt/live/cert_name/*.pem /certs/
chown root:8080 /certs/*
chmod 640 /certs/privkey.pem

# run container temporarily with TLS 1.3 (no need to generate dhparam.pem)
docker run --rm \
  -p 80:80 \
  -p 443:443 \
  -v /certs:/certs:ro \
  -e ACCESS_LOG=ON \
  -e SERVER_NAMES="example.com,www.example.com" \
  -e TLS13_ONLY=TRUE \
  asifbacchus/ab-nginx

Open your browser and test things out. You should get the container default greeting page served over HTTPS automatically. Assuming thats all good, you can now start the container for real. Hit ctrl-c on your host to stop the foreground container and remove it. Now lets get things mounted properly, for example:

# run with TLS 1.3-only
docker run -d --name ab-nginx --restart unless-stopped \
  -p 80:80 \
  -p 443:443 \
  -v /certs:certs:ro \
  -v /var/www:/usr/share/nginx/html \
  -v ~/nginx/config:/etc/nginx/config:ro \
  -e SERVER_NAMES="example.com,www.example.com" \
  -e TLS13_ONLY=TRUE \
  asifbacchus/ab-nginx

Obviously, add or remove any bind-mounts you may need you should already know how to do that from the previous 2 pages. Assuming everything is working and TLS 1.3-only is fine for you, then youre done!

If you need TLS 1.2, then our last step is generating dhparam.pem, copying it and restarting our container.

# generate dhparam.pem and fix permissions
cd /certs
openssl dhparam -dsaparam -out ./dhparam.pem 4096
chown root:8080 dhparam.pem
chmod 644 dhparam.pem

# run with TLS 1.2+1.3
docker run -d --name ab-nginx --restart unless-stopped \
  -p 80:80 \
  -p 443:443 \
  -v /certs:certs:ro \
  -v /var/www:/usr/share/nginx/html \
  -v ~/nginx/config:/etc/nginx/config:ro \
  -e SERVER_NAMES="example.com,www.example.com" \
  asifbacchus/ab-nginx

Ok, thats actually it! Enjoy running with free certificates!