Categories
DevOps Server SysAdmin

Simple certs with Docker-Dehydrated

Easily create (and automatically renew) certificates from the Let’s Encrypt Certificate Authority and simultaneously distribute them to all of your servers on a schedule.

I have a separate administrative server that’s responsible for maintaining certificates via the Matrix organization‘s Docker-Dehydrated image, and then distributing those certificates to the appropriate servers. This article covers the view from my administrative server, but if you’re running a single server, then you can place it alongside Traefik, Nginx, et al. and forgo the distribution section at the end of this article.

Prerequisites

Setup

Docker Compose

The highlighted lines need to be changed to suit your needs.

Let’s Encrypt has rate limits, which is why you’ll want to test your configuration before you switch over to production. When you’re certain that everything is working, swap out dehydrated-testing.env file with dehydrated-production.env

/etc/docker/compose/dehydrated/docker-compose.yaml

version: '2'
services:

    dehydrated:
        container_name: dehydrated
        image: docker.io/matrixdotorg/dehydrated
        restart: unless-stopped

        volumes:
            - /srv/dehydrated/accounts/:/data/accounts/
            - /srv/dehydrated/certs/:/data/certs/
            - /srv/dehydrated/domains.txt:/data/domains.txt

        env_file:
            - dehydrated.env
            - dehydrated-testing.env
            # - dehydrated-production.env

/etc/docker/compose/dehydrated/dehydrated.env

DEHYDRATED_GENERATE_CONFIG=yes
DEHYDRATED_CHALLENGE=dns-01
DEHYDRATED_KEYSIZE=4096
DEHYDRATED_HOOK=/usr/local/bin/lexicon-hook
DEHYDRATED_RENEW_DAYS=30
DEHYDRATED_KEY_RENEW=yes
DEHYDRATED_EMAIL=YOU@EXAMPLE.COM
DEHYDRATED_ACCEPT_TERMS=no
PROVIDER=digitalocean
LEXICON_DIGITALOCEAN_TOKEN=0123456789

Docker-Dehydrated uses Dehydrated for Let’s Encrypt and Lexicon for DNS operations supported by many providers.

If you’re using a provider other than Digital Ocean, you’ll need to swap out LEXICON_DIGITALOCEAN_TOKEN with the appropriate environment variables. For example, if you were to set PROVIDER to cloudflare, you would use LEXICON_CLOUDFLARE_USERNAME and LEXICON_CLOUDFLARE_TOKEN.

Double-click to select all, copy, and paste to your own files:

/etc/docker/compose/dehydrated/dehydrated-testing.env

DEHYDRATED_CA="https://acme-staging-v02.api.letsencrypt.org/directory"

/etc/docker/compose/dehydrated/dehydrated-production.env

DEHYDRATED_CA="https://acme-v02.api.letsencrypt.org/directory"

Dehydrated

Now that the Docker Compose side has been configured, set up your data directories and domain configuration:

mkdir -pv /srv/dehydrated/{accounts,certs} \
&& \
touch /srv/dehydrated/domains.txt

Populate /srv/dehydrated/domains.txt with the domains that you want to use, with one domain on each line, and spaces that separate SANs (Subject Alternative Name) or a wildcard domain (which is only available via DNS-based confirmation).

To set up certificates for Thad.Getterman.org and LTG.FYI, I have the following:

thad.getterman.org    www.thad.getterman.org
ltg.fyi               *.ltg.fyi

In this example, I’ve created a single SAN so that if someone places www in front of my name, it will still work. While as with my URL-shortening domain, I’ve created a wildcard domain, so that I can direct traffic based on not only the path but also any sub-domain, all while providing encryption for any combination that’s queried from Traefik.

Now you can see if it works with dehydrated-testing.env enabled:

cd /etc/docker/compose/dehydrated

docker-compose up && docker-compose down

After several minutes, and if everything worked correctly, you should see success messages about certificates being established for your domain(s):

dehydrated    | RESULT
dehydrated    | ------
dehydrated    | True
dehydrated    |  + Requesting certificate...
dehydrated    |  + Checking certificate...
dehydrated    |  + Done!
dehydrated    |  + Creating fullchain.pem...
dehydrated    | deploy_cert called: thad.getterman.org, /data/certs/thad.getterman.org/privkey.pem, /data/certs/thad.getterman.org/cert.pem, /data/certs/thad.getterman.org/fullchain.pem, /data/certs/thad.getterman.org/chain.pem
dehydrated    |  + Done!
dehydrated    | RESULT
dehydrated    | ------
dehydrated    | True
dehydrated    |  + Requesting certificate...
dehydrated    |  + Checking certificate...
dehydrated    |  + Done!
dehydrated    |  + Creating fullchain.pem...
dehydrated    | deploy_cert called: ltg.fyi, /data/certs/ltg.fyi/privkey.pem, /data/certs/ltg.fyi/cert.pem, /data/certs/ltg.fyi/fullchain.pem, /data/certs/ltg.fyi/chain.pem
dehydrated    |  + Done!

Press ^C (CTRL-C) on your keyboard, and Docker Compose will exit and then remove the container. Assuming that everything worked correctly, now expunge the data in preparation for production use:

for currDir in accounts certs; do
	rm -rfv "/srv/dehydrated/${currDir}/"*
done

In docker-compose.yaml, disable dehydrated-testing.env, and enable dehydrated-production.env in its place.

Then, enable, start, and tail the Dehydrated systemd-based Docker Compose service:

{
    systemctl enable docker-compose@dehydrated
    systemctl start docker-compose@dehydrated
    journalctl -f -u docker-compose@dehydrated
}

After several minutes, and similar to the prior messages from the testing server, you should see the domain certificates begin to populate. Press ^C (CTRL-C) to exit tailing the systemd log for this service. This service will run indefinitely, and when a certificate needs to be renewed, will do so, and store it at /srv/dehydrated/certs:

/srv/dehydrated/certs
├── ltg.fyi
│   ├── cert-0123456789.csr
│   ├── cert-0123456789.pem
│   ├── cert.csr -> cert-0123456789.csr
│   ├── cert.pem -> cert-0123456789.pem
│   ├── chain-0123456789.pem
│   ├── chain.pem -> chain-0123456789.pem
│   ├── combined.pem
│   ├── fullchain-0123456789.pem
│   ├── fullchain.pem -> fullchain-0123456789.pem
│   ├── privkey-0123456789.pem
│   └── privkey.pem -> privkey-0123456789.pem
└── thad.getterman.org
    ├── cert-0123456789.csr
    ├── cert-0123456789.pem
    ├── cert.csr -> cert-0123456789.csr
    ├── cert.pem -> cert-0123456789.pem
    ├── chain-0123456789.pem
    ├── chain.pem -> chain-0123456789.pem
    ├── combined.pem
    ├── fullchain-0123456789.pem
    ├── fullchain.pem -> fullchain-0123456789.pem
    ├── privkey-0123456789.pem
    └── privkey.pem -> privkey-0123456789.pem

Distribution

Save the following script to /usr/local/bin/distro-certs.bash

#!/usr/bin/env bash

parallel-rsync \
    --user=root \
    --host 1.1.1.1 \
    --host 2.2.2.2 \
    --host 3.3.3.3 \
    --host 4.4.4.4 \
    --host 5.5.5.5 \
    --host 6.6.6.6 \
    --host 7.7.7.7 \
    --host 8.8.8.8 \
    --host 9.9.9.9 \
    --extra-arg "--delete" \
    --extra-arg "--no-owner" \
    --extra-arg "--no-group" \
    --extra-arg "--hard-links" \
    --archive \
    --recursive \
    --compress \
    "/srv/dehydrated/certs/" \
    "/srv/letsencrypt/" \
    ;

Set up a Cron job for this script to run as you see fit with crontab -e and use crontab guru to come up with a time that best suits you. Personally, I like to synchronize my certificates once per day:

24 12 * * * /usr/local/bin/distro-certs.bash > /var/log/distro-certs.log

Now our nine servers defined in the above script will have copies of these server certificates at /srv/letsencrypt/ which can be used by Traefik, Nginx, et al. — to see how I use this for Traefik, check out my next article, Traefik 2.5 quick-start guide.


Did this article save you time or money? I'd love a coffee!

Did you find this useful?
Please share with those who you believe would find this useful too!

Leave a Reply

Your email address will not be published. Required fields are marked *