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 .