Let's Encrypt Everything!

Posted by Brian on February 26, 2017

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!