Let's Encrypt Your Docker

What is Let's Encrypt?

A new Certificate Authority that is free, automated, and open. Check out the Let's Encrypt website for more information.

How?

Running on CoreOS, I Dockerize everything. I also use nginx. Let's Encrypt's full automation doesn't work out of the box with either of these aspects. So to user Let's Encrypt with Docker, I needed to take another approach. My Let's Encrypt with Docker setup is based on this great blog post that showed me how to use the webroot method to negotiate the Let's Encrypt protocol.

This method works great because it enables us to keep our nginx container running while the Let's Encrypt process runs. We do this by mounting the Let's Encrypt folder into the web container so that nginx can serve up the files generated by the Let's Encrypt container. That same mount will put the SSL certificates inside the container so that they are accessible.

You will first need to run the Let's Encrypt process without SSL enabled in your web container, because you can't start the container once you specify your certificate files.

Dockerfile

The Dockerfile is simple, using a public Let's Encrypt image as a base, and then adding our custom configuration file.

FROM quay.io/letsencrypt/letsencrypt:latest  
COPY webroot.ini /  

webroot.ini

Here is the custom configuration file

rsa-key-size = 2048  
server = https://acme-v01.api.letsencrypt.org/directory  
email = your_email_to_get_renewal_notices@example.com  
text = True  
agree-tos = True  
agree-dev-preview = True  
renew-by-default = True  
authenticator = webroot  

Nginx configuration

In your http block, add the following to support good, strong SSL protocols:

ssl_protocols TLSv1.2 TLSv1.1 TLSv1;  
ssl_prefer_server_ciphers on;  
ssl_ciphers 'kEECDH+ECDSA+AES128 kEECDH+ECDSA+AES256 kEECDH+AES128 kEECDH+AES256 kEDH+AES128 kEDH+AES256 DES-CBC3-SHA +SHA !aNULL !eNULL !LOW !MD5 !EXP !DSS !PSK !SRP !kECDH !CAMELLIA !RC4 !SEED';  
ssl_session_cache shared:SSL:20m;  
ssl_session_timeout 10m;  
ssl_dhparam /etc/nginx/ssl/dhparam.pem;  

Notice I'm specifying custom dhparams to fight against the logjam attack, here is the rundown on that:

Run this on your machine in the folder where your Dockerfile is

openssl dhparam -rand - 2048 >> dhparam.pem  

Then add this to your Dockerfile

COPY dhparam.pem /etc/nginx/ssl/dhparam.pem  

In your server blocks (for both port 80 and 443, make sure your domain A records point to the server block that will respond for this request.

location /.well-known/acme-challenge {  
    root /etc/letsencrypt/webrootauth;
   default_type "text/plain";
}

Modify the command to start your web container and add the volume mount for Let's Encrypt. This will be in your web.service file for systemd if your setup is like mine. Then restart your web service.

-v "/etc/letsencrypt:/etc/letsencrypt"

Running it

Here is how we invoke our Let's Encrypt container to run the protocol using the webroot plugin. Replace domain.com with your own domain name. You can specify the -d flag more than once for Subject Alternative Name (SAN), where the first -d flag is the main domain and the others are legitimate alternatives for the certificate to be valid for. You can specify up to 100 of them!

docker run --rm --name letsencrypt -v "/var/log/letsencrypt:/var/log/letsencrypt" -v "/etc/letsencrypt:/etc/letsencrypt" -v "/var/lib/letsencrypt:/var/lib/letsencrypt" letsencrypt:latest --webroot-path "/etc/letsencrypt/webrootauth" -c "/webroot.ini" -d domain.com certonly  

Using it

Once you have your certificates, add these lines to your 443 server block in your nginx configuration to use them! Replace domain.com with your own domain name.

ssl_certificate_key /etc/letsencrypt/live/domain.com/privkey.pem;  
ssl_certificate /etc/letsencrypt/live/domain.com/fullchain.pem;  

Automate it

Let's Encrypt certificates expire after 90 days, so automation of renewing your certificates is important. Here is the setup for a systemd timer and service to renew the certificates and reboot the nginx Docker container. Replace domain.com with your own domain name.

The service

[Unit]
Description=letsencrypt cert update oneshot  
Requires=docker.service

[Service]
Type=oneshot  
ExecStart=/usr/bin/docker run --rm --name letsencrypt -v "/var/log/letsencrypt:/var/log/letsencrypt" -v "/etc/letsencrypt:/etc/letsencrypt" -v "/var/lib/letsencrypt:/var/lib/letsencrypt" letsencrypt:latest --webroot-path "/etc/letsencrypt/webrootauth" -c "/webroot.ini" -d domain.com certonly  
ExecStartPost=-/usr/bin/systemctl restart web.service  

The service needs to restart the web service when it is done running so that nginx will reload the new certificate files.

The Timer

[Unit]
Description=letsencrypt oneshot timer  
Requires=docker.service

[Timer]
OnCalendar=*-*-1 06:00:00

[Install]
WantedBy=timers.target  

You can specify whatever calendar you want, but the first of the month in the early morning seems to make sense to me.

Conclusion

So far, Let's Encrypt is working really well for me. It is automatic, and it fits well within my existing infrastructure and configuration. SSL is becoming more common (a very good thing) and more expected (another good thing). With Let's Encrypt, we are out of excuses. Adding SSL is now free, easy, and renewals are free from headache. The tools are now available and free, so Let's Encrypt.