Up until now I encrypted only one of my websites with a GeoTrust RapidSSL certificate I purchased each year. I wanted to enable HTTPS (SSL/TLS) for all my websites and started looking at Let’s Encrypt as a possible solution.
To use the free Let’s Encrypt certificate authority, software that supports the ACME protocol needs to be running on the web server in order to automatically obtain and renew certificates.
Certbot is the recommended software for this purpose, so this was the first place that I started. While Certbot has a Nginx plugin for the web server I’m using, it is currently alpha code and Certbot website recommend using the “webroot” plugin for my Software/System combination.
The kvaps/letsencrypt-webroot image is a nice implementation of Certbot configured to use the “webroot” plugin. Unfortunately, I ran into a problem where the Nginx web server would not run because the referenced SSL certificates did not yet exist. One possible option was to temporarily disable the SSL configuration so that Nginx would run and then fix it back once the initial certificates were fetched.
Since I am using Docker I really wanted a solution that will work right away when I run the containers, so I looked at some other ACME clients. One which I have used successfully for another project is dehydrated.
The lelandsindt/hydrator image uses dehydrated and Nginx together in an elegant way to automatically fetch and renew SSL certificates from Let’s Encrypt. Here are some samples from the configuration that worked for me.
docker-compose.yml
version: '2'
services:
web:
restart: always
build: ./web
ports:
- "80:80"
- "443:443"
volumes:
- dehydrated-certs:/etc/dehydrated/
volumes:
dehydrated-certs:
The certificates and related files are stored in the dehydrated-certs
named volume for persistance between restarts of the container.
web/Dockerfile
FROM lelandsindt/hydrator
# set timezone
RUN apk add -U tzdata && cp /usr/share/zoneinfo/America/New_York /etc/localtime
# Hack the Hydrator script
RUN sed -i '/#!\/bin\/bash/a cp /usr/local/etc/dehydrated/* /etc/dehydrated/' /usr/bin/hydrator
# Add DH params (generated with openssl dhparam -out dhparams.pem 2048)
COPY ssl/dhparams.pem /etc/ssl/private/
# Add dehydrated config files
COPY dehydrated/ /usr/local/etc/dehydrated/
# add custom nginx config
ADD nginx/conf.d/ /etc/nginx/conf.d/
# copy in web content
ADD html/ /var/www/html/
The “Hack the Hydrator script” portion of web/Dockerfile
solves the one problem I ran into where the script expects configuration to be stored in /etc/dehydrated
. The line added to the script simply copies the supplied config
and domains.txt
files into place on the dehydrated-certs
named volume.
web/dehydrated/config
CA="https://acme-v01.api.letsencrypt.org/directory"
CA_TERMS="https://acme-v01.api.letsencrypt.org/terms"
CONTACT_EMAIL="youremail@YOURDOMAIN.com"
OCSP_MUST_STAPLE="yes"
DOMAINS_TXT="/etc/dehydrated/domains.txt"
The DOMAINS_TXT
configuration line is probably not needed since this is where dehydrated looks by default, but it helps to document how things are working.
If you are using these configuration samples for your own website, be sure to replace YOURDOMAIN.com
with your own domain name. Also of note is you should use the staging server for the CA
and CA_TERMS
fields in the config file when testing things out.
CA="https://acme-staging.api.letsencrypt.org/directory"
CA_TERMS="https://acme-staging.api.letsencrypt.org/terms"
web/dehydrated/domains.txt
YOURDOMAIN.com www.YOURDOMAIN.com
otherdomain.xyz www.otherdomain.xyz
Here is where each of the certificates needed are listed with any applicable Subject Alternative Names (SANs). Typically this would be the bare domain name and the www
variation if desired.
web/conf.d/default.conf
server {
listen 443 default_server ssl http2;
server_name YOURDOMAIN.com www.YOURDOMAIN.com;
# certs sent to the client in SERVER HELLO are concatenated in ssl_certificate
ssl_certificate /etc/dehydrated/certs/YOURDOMAIN.com/fullchain.pem;
ssl_certificate_key /etc/dehydrated/certs/YOURDOMAIN.com/privkey.pem;
ssl_session_timeout 1d;
# Conflicts with https://github.com/alpinelinux/aports/blob/master/main/nginx/nginx.conf#L67
# ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
ssl_dhparam /etc/ssl/private/dhparams.pem;
# intermediate configuration. tweak to your needs.
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
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:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
add_header Strict-Transport-Security max-age=15768000;
# OCSP Stapling ---
# fetch OCSP records from URL in ssl_certificate and cache them
ssl_stapling on;
ssl_stapling_verify on;
## verify chain of trust of OCSP response using Root CA and Intermediate certs
ssl_trusted_certificate /etc/dehydrated/certs/YOURDOMAIN.com/fullchain.pem;
resolver 8.8.8.8;
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# prefer the www flavor of the website
if ($host = YOURDOMAIN.com) {
rewrite ^(.*) https://www.YOURDOMAIN.com:443$request_uri? permanent;
}
location / {
root /var/www/html;
index index.html index.htm;
}
error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /var/lib/nginx/html;
}
}
The SSL portion of the configuration was obtained from the Mozilla SSL Configuration Generator with a few modifications. For example, the ssl_session_cache
setting conflicts with one in the Alpine Linux default nginx.conf file.
When the containers are first run, the Nginx instance that handles the websites keeps stopping because of the missing SSL certificates. However, dehydrator spins up a separate Nginx instance to validate the domains with Let’s Encrypt. In a few seconds, the Nginx instance has the certificates it needs from Let’s Encrypt and the websites are up and running with HTTPS (SSL/TLS) encryption. All thanks to hydrator, dehydrated, and Let’s Encrypt!