]> Automating LetsEncrypt.org with DNS verification 🌐:aligrant.com

Automating LetsEncrypt.org with DNS verification

Alastair Grant | Tuesday 14 November 2017

Let's Encrypt provides free, signed x509 certificates to use for pretty much whatever you want.  Most commonly, TLS encryption on web-servers.  Unlike traditional Certificate Authorities which sign certificates for a number of years, Let's Encrypt use a 90-day validity period.

From running a private CA, this filled me with dread, as changing certificates can be a bit of a pain.  With a 90-day validity period there is only one possible option: automation.  Let's Encrypt use "Automatic Certificate Management Environment (ACME)" protocol, which at the time of writing is still in draft.  This provides an easy-to-use JSON web-service for registering and renewing certificates.

There are various tools that implement the ACME protocol, with certbot being the poster-child.  I've spent a bit of time trying out these tools to find out what works best for me.  My requirement is to generate certificates for all sorts of services, not just HTTP, which means using the standard HTTP verification method is not feasible.  Instead I need to use DNS (or, DNS-01 in the protocol).  This inserts the validation token into a DNS TXT record - which demonstrates your control over the domain.

I am further complicating things by not having my DNS server on the same server as the services I require certificates for (I know, crazy, right?) - which many of the automated script rely on.

In the end, I've settled on getssl as the script to do the work for me.  It is simply a Bash script for automating the certificate process.

The official documentation details how to setup configuration files for use, so I won't go into that.  What I will cover is a few tweaks to that process.

Private Key

You can let the script generate a key, but I like to generate one myself:

openssl ecparam -name secp256r1 -genkey -noout > ~/.getssl/mydomain.com/mydomain.com.key

Certificate Signing Request

You can also use, optionally, your own CSR - which allows you to customise a lot, but what Let's Encrypt will allow is down to them. I use openssl with this template to build a CSR:

[req]
default_bits = 2048
default_md = sha256
distinguished_name = req_distinguished_name
req_extensions = ssl_server

[req_distinguished_name]
commonName = Common name
0.organizationName = Display name

[ssl_server]
basicConstraints = CA:FALSE
nsCertType = server
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName=@subjAlt

[subjAlt]
DNS.1 = aligrant.com
DNS.2 = subdomain.aligrant.com

It is important that all domains you want to use are listed as subjectAltName - as it is this field that many browsers will check and not the commonName (although everything should fall back to commonName, it doesn't always happen that way).

Generate your CSR and fill in the fields:

openssl req -new -sha256 -x509 -config myconfig.conf -key ~/.getssl/mydomain.com/mydomain.com.key -out ~/.getssl/mydomain.com/mydomain.com.csr

Configuring BIND DNS

You will need to use dynamic DNS updates to allow your token to be written into DNS.  In order to do this, you want to secure your zone against a relevant key.

Generate a new TSIG key:

dnssec-keygen -a HMAC-MD5 -b 512 -n HOST letsencrypt

This will generate two files, we're interested in the .key file.  We need to copy the Base64 encoded string at the end and put it into a new file, letsencrypt.tsig:

key letsencrypt {
algorithm hmac-md5;
        secret "VGhlIGZpcnN0IHRoaW5nIHRvIHJlbWVtYmVyIGlzOiBhbHdheXMgdHJlYXQgeW91ciBraXRlIGxpa2UgeW91IHRyZWF0IHlvdXIgd29tYW4h";
};

You can then reference this file in your /etc/named.conf:

include "/etc/named.d/letsencrypt.tsig";

zone "mydomain.com" in {
  type master;
  allow-update { key "letsencrypt"; };
  file "mydomain.com";
};

With this in place, you can use nsupdate with the TSIG file to issue dynamic updates to the zone.

nsupdate Scripts

There are two sample scripts in the getssl project for wrapping nsupdate, I've altered them slightly to fit my needs:

#!/bin/bash

dnskeyfile="$1"
fulldomain="$2"
token="$3"

updatefile=$(mktemp)

printf "update add _acme-challenge.%s. 1 in TXT \"%s\"\n\n" "${fulldomain}" "${token}" > "${updatefile}"

nsupdate -k "${dnskeyfile}" -v "${updatefile}"

rm -f "${updatefile}"

sleep 5

I've added the ability to pass in the TSIG file dynamically (as it may be different for different domains) as well as sleeping for five seconds at the end.  It is normal to have at least two or three nameservers for a domain, but you generally only update one and have that information filter out to the others.  Whilst that process is very quick, there is a delay to it.  Adding a sleep to the script gives time for all the servers to have the latest update before Let's Encrypt checks them, as otherwise you may receive a NXDOMAIN error (can't find the record) and verification will fail.

Change the "add" to "delete" in the above script to delete the record.

Calling the update

The final step is to setup your configuration file (e.g. ~/.getssl/mydomain.com/mydomain.com.conf) to use DNS-01 updates.

# Comment out the ACL entry, as these aren't relevant for DNS
#ACL=(...)

VALIDATE_VIA_DNS="true"
DNS_ADD_COMMAND="~/nsupdate_add.sh letsencrypt.tsig"
DNS_DEL_COMMAND="~/nsupdate_del.sh letsencrypt.tsig"

Something similar to the above, in addition to the usual fields you would configure.  This will result in the above script being run with the appropriate TSIG file.  The extra parameters are handled automatically by the getssl script itself.

Finishing off

"getssl" provides plenty of customisation without excessive hand-holding.  You can configure it to put certificates on remote servers and restart services etc.

Simply setup a cronjob to run the script against your domain(s) once a week and it'll keep on top of checking how much life they have left and renewing as appropriate.

Breaking from the voyeuristic norms of the Internet, any comments can be made in private by contacting me.