Note to Self

Automate the verification of Let's Encrypt wildcard certificates

Automating the verification of wildcard certificates is not as straightforward as that of simple ones, because you cannot simply drop a file into a directory and be done with it.

I have devised a mechanism to do it. I am not sure if it is the most elegant way to do it, but it does work. It requires you to have control of the DNS zones for the domains you want to secure, and to have one DNS zone (can be any one; you can register a cheap domain especially for the purpose) you can control programmatically. In my case, I run a name server on the same server I run the certificate creation and renewal on, and I can change zone files and reload the server from a script.

I am using the following names in the example:

Function Example used
Domain I want to secure with a wild card certificate emeademo.eu
Domain I can control programmatically acme-challenge.internetcraft.net
Server that is the only name server for that domain prokupac.internetcraft.net
E-mail address I use for the hostmaster hostmaster@internetcraft.net

As a preparation, in the DNS zones of emeademo.eu , you need to add:

_acme-challenge CNAME   emeademo.eu.acme-challenge.internetcraft.net.

You can do this at any time, and you only need to do it once. This delegates the verification to a subdomain of the domain you can control programmatically. Depending on where the DNS zone for your domain is hosted, you may instead have to create a subdomain _acme-challenge and create a CNAME entry that points to emeademo.eu.acme-challenge.internetcraft.net. in the web interface of your hosting provider, but the result will be the same.

When the time for verification comes, a script will be called to create a zone file for the target of the delegation. Here's the script that does that:

#! /usr/bin/perl

$serial = time() - 946684800;

$domain = $ENV{'CERTBOT_DOMAIN'};
$challenge = $ENV{'CERTBOT_VALIDATION'};

open(ZONE, "</etc/bind/local-master/acme-challenge.internetcraft.net") || die $_;
@zone = <ZONE>;
close(ZONE);

open(ZONE, ">/etc/bind/local-master/acme-challenge.internetcraft.net") || die $_;

for(@zone)
{
        if(/SOA/)
        {
                print ZONE "@       IN      SOA     prokupac.internetcraft.net.     hostmaster.internetcraft.net. ( $serial 60 300 900 60 )\n";
        }
        elsif(!/^$domain\t/)
        {
                print ZONE;
        }
}

print ZONE "$domain     TXT     \"$challenge\"\n";

close(ZONE);

system('/etc/bind/restart-bind9');

So this rewrites the zone file for acme-challenge.internetcraft.net on the fly and restarts the name server. How to do that is left as an exercise to the reader. In my case, that's a C program that belongs to root and is SUID. Restarting is probably overkill; reloading would be enough. But I had the C program installed for another purpose anyway.

Also left as an exercise to the reader is how to configure the name server so this file is the active zone file for the domain, and is writable to the verification process. The number 946684800 is not magic in any way; it only needs to be smaller than the UNIX time at the time of the first verification and large enough so the zone serial number does not become too large. It is also important not to change it later, at least not so that the next generated serial becomes smaller that the last one. Come to think of it, I could probably just grab the existing serial and increment it by one instead of generating it from the current time.

This is what the zone file looks like (shortened and sanitized):

$TTL    60
@       IN      SOA     prokupac.internetcraft.net.     hostmaster.internetcraft.net. ( 601648548 60 300 900 60 )
        NS      prokupac.internetcraft.net.
emeademo.eu     TXT     "super-secret-string-that-will-be-different-every-time"
other-domain.tld    TXT "another-super-secret-string-that-will-be-different-every-time"

So the script removes any old entries for the domain currently being verified and appends a new one to the end. Again, the 601648548 is not magic and will be changed by the script at the next verification.

I considered to clean up the entry after the verification and provided a cleanup script manual-cleanup-hook.sh to do it, but then reconsidered and left that empty. Possibly, the whole trick works entirely without a cleanup script, but I don't know and haven't tried.

The final piece of the puzzle is the certbot command line. Here it is:

certbot --server https://acme-v02.api.letsencrypt.org/directory \
    -d "*.emeademo.eu" \
    --agree-tos \
    --email hostmaster@internetcraft.net \
    --non-interactive \
    --manual-public-ip-logging-ok \
    --manual \
    --manual-auth-hook DIR/manual-auth-hook.pl \
    --manual-cleanup-hook DIR/manual-cleanup-hook.sh \
    --preferred-challenges dns-01 \
    certonly

As you can see, I prefer not to install the certificates automatically, as they will be distributed internally to and used on a number of systems. You can read more in Get wildcard certificates on a server .