I have been using Let’s Encrypt’s DNS-01 challenge to prove that I control DNS for domain names. However, obtaining certificates and renewing them have been a manual process where I added scripts to getssl
to tell me the TXT record(s) to add for that domain. I did it this way because I did not like the idea of giving a script full capability (at the time) to play with my DNS records. I do not remember where, but I learned that we can CNAME TXT records which means that a TXT record can be a CNAME pointing at another record that can be updated. Neat.
Plan
- Set up a BIND 9 server
- Set up a dynamic zone for acme.example.net
- Generate ddns keys for servers that host a service that uses an SSL certificate
- Update DNS
- Add A and NS records such that acme.example.net queries are handled by another nameserver
- Add CNAME records for
_acme-challenge.foo.example.net
to point at_acme-challenge.foo.acme.example.net
- Request certificates for a given set of domains
Most of what I did was influenced by Dan Langille’s post at https://dan.langille.org/2019/02/01/acme-domain-alias-mode/.
BIND 9
Install
On Debian, installing BIND 9 was done by executing sudo apt install bind9
.
Disable recursion
One of the things that we need to do is to disable recursion such that our nameserver is not an open resolver. Recursion is disabled by adding the following to the options
block in /etc/bind/named.conf.options
recursion no;
Set up a dynamic zone
Copy /etc/bind/db.empty
to /var/lib/bind/db.acme.example.net
and edit accordingly.
$TTL 86400
@ IN SOA a.ns.example.net. no-reply.acme.example.net. (
2020010700 ; Serial
43200 ; Refresh
7200 ; Retry
2419200 ; Expire
86400 ) ; Negative Cache TTL
;
@ IN NS a.ns.example.net.
db.acme.example.net
will be light since it is a dynamic zone.
Update /etc/bind/named.conf.local
with the dynamic zone
zone "acme.example.net." {
type master;
file "/var/lib/bind/db.acme.example.net";
};
ddns key
We will now generate a dynamic DNS key that will allow our ACME client of choice to dynamically update the dynamic zone we have created.
On Arch Linux, I used the following ddns-confgen
invocation to generate a ddns key
ddns-confgen -a hmac-sha512 -k one.example.net. -q > one.example.net.key
The contents of one.example.net.key
key "one.example.net." { algorithm hmac-sha512; secret "iXdUnXCbxJqcr3rgxYVdvHVqLmJkHW6mNZEDsXwDXtvxIfVmoABHWjN4ko4+Rz2uEmgOk+IFfqgEVGrAYTWaIA=="; };
Now that we have a ddns key, we will need to make BIND 9 aware of it.
In /etc/bind/named.conf
, we will add an include for a new file at /etc/bind/named.conf.keys
include "/etc/bind/named.conf.keys";
The content of /etc/bind/named.conf.keys
will be the contents of one.example.net.key
. Setting it up this way allows us to add named.conf.keys
to .gitignore
if we’re using configuration management such as Ansible.
Last but not least, we will update the zone configuration in /etc/bind/named.conf.local
with an update-policy
that says this key (one.example.net.) is permitted to update this zone:
zone "acme.example.net." { type master; file "/var/lib/bind/db.acme.example.net"; update-policy { grant one.example.net. zonesub TXT; }; };
With the above configuration, the one.example.net.
key would be able to update any TXT record within the acme.example.net zone. If desired, we could restrict the key to a domain such as assets.example.net, we would replace the grant line with the following:
grant one.example.net. name _acme-challenge.assets.acme.example.net. TXT;
Add DNS records
To delegate the _acme-challenge
record to a subdomain, I added the following DNS records to the nameserver serving example.net:
a.ns IN A 192.168.26.42
acme IN NS a.ns.example.net.
Now for each domain I want to delegate the _acme-challenge
record to, I added a CNAME like the following:
_acme-challenge.subdomain IN CNAME _acme-challenge.subdomain.acme.mydomain.net.
Request Certificates
At this point, we upload one.example.net.key
to our box. I stash mine as /root/nsupdate.key
. Then I use the following acme.sh invocation to request a certificate for one.example.net
.NSUPDATE_SERVER
and NSUPDATE_KEY
are environment variables to clue acme.sh
to the name server to update and the key to use.
# NSUPDATE_SERVER=a.ns.example.net NSUPDATE_KEY=/root/nsupdate.key acme.sh --issue --dns dns_nsupdate -d one.example.net --challenge-alias one.acme.example.net --server letsencrypt --reloadcmd 'systemctl reload nginx'
With acme.sh
we could use letsencrypt_test
as the --server
so that we can test to make sure things are working as expected.