LetsEncrypt DNS verification using a local BIND instance
I’ve been looking into Let’s Encrypt DNS verification for a while.
Not only because you’re able to obtain wildcard certificates through
this method, freeing you from the necessity to obtain an individual
certificate for every single one of your subdomains: It also allows you
to get a certificate for stuff running on your LAN, provided you’re
running it on a subdomain that belongs to you. The problem is though,
how do you enable Certbot to automate the DNS server update, without
putting a credential in place that would allow full access to all your
domains? And what to do if you’re running a server for a domain that
doesn’t even belong to you: How can the owner delegate permissions for
the verification TXT records to you, without having to give you full
access to all their domains? Today I stumbled across a solution:
Delegate the _acme-challenge
subdomain to a local BIND
instance and have Certbot update that. Here’s how.
So, the basic idea as outlined above (and stolen from this
guy) is to delegate the _acme-challenge
name into a
subzone on a local BIND. This is easily achieved by adding an
NS
record to the zone, like so:
_acme-challenge.example.com. 300 IN NS tiamat.example.com.
tiamat
is the host on which I want to run Certbot and
BIND. Thus, bind needs to be set up and it needs to know the zone.
Bind setup
(This part is also mostly outlined in the official certbot documentation, but with a few Debian-specific things missing.)
First of all, apt-get install bind9
and go to the
/etc/named
directory. There we’ll have to generate a key
that is used to secure the update process:
dnssec-keygen -a HMAC-SHA512 -b 512 -n HOST certbot.
Update: A better method to do this might be:
rndc-confgen -a -A hmac-sha512 -k "certbot." -c /etc/bind/certbot.key
This generates the
key
stanza shown below, so you don’t have to write it manually.
This will generate two files: Kcertbot.+165+39568.key
and Kcertbot.+165+39568.private
, which appear to make up a
public/private key pair? Not sure. Anyway, the .key
file
has a certbot. IN KEY
record which we are interested in,
because that key will show up in the other configs that we’re about to
create.
I put the zone configuration into
named.conf.certbot
:
key "certbot." {
algorithm hmac-sha512;
secret "here goes the secret from the .key file";
};
zone "_acme-challenge.example.com" {
type master;
file "/var/lib/bind/db.example.com";
allow-query { any; };
update-policy {
grant certbot. name _acme-challenge.example.com. txt;
};
};
I placed the db file in /var/lib/bind
because Bind will
want to create a Journal file and update the Zone file as it goes, which
is forbidden by Apparmor if files are placed in
/etc/bind
.
Here’s the Zonefile I started with:
$ORIGIN .
$TTL 300 ; 5 minutes
_acme-challenge.example.com IN SOA tiamat.example.com. email-address.example.com. (
2020050806 ; serial
10800 ; refresh (3 hours)
3600 ; retry (1 hour)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
NS tiamat.example.com.
$TTL 60 ; 1 minute
TXT "127.0.0.1"
Note: I received word that the TXT
record here might actually cause problems by shadowing the one that
Certbot will create. For me it works, but if it doesn’t work for you for
some reason, try deleting it. I didn’t put it in for any specific reason
other than not being sure if it can be omitted without breaking the db
file syntax by not having an entry for the $TTL 60
to refer
to. Thanks, Norbert!
Be sure to include the file in /etc/bind/named.conf
:
include "/etc/bind/named.conf.certbot";
Validate the configs:
# named-checkzone _acme-challenge.example.com. /var/lib/bind/db.example.com
zone _acme-challenge.example.com/IN: loaded serial 2020050806
OK
# named-checkconf
#
Now restart bind and you should be good to go.
Verifying bind
When requesting a certificate, Certbot will try to figure out which Zone it needs to update by guessing a few names and querying their SOA record from the DNS server. If you run into an error such as this one, this means that your DNS server does not deliver the SOA record correctly:
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/certbot/auth_handler.py", line 75, in handle_authorizations
resp = self._solve_challenges(aauthzrs)
File "/usr/lib/python3/dist-packages/certbot/auth_handler.py", line 139, in _solve_challenges
resp = self.auth.perform(all_achalls)
File "/usr/lib/python3/dist-packages/certbot/plugins/dns_common.py", line 57, in perform
self._perform(domain, validation_domain_name, validation)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 76, in _perform
self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 112, in add_txt_record
domain = self._find_domain(record_name)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 190, in _find_domain
.format(record_name, domain_name_guesses))
certbot.errors.PluginError: Unable to determine base domain for _acme-challenge.example.com using names: ['_acme-challenge.example.com', 'example.com', 'com'].
You can verify this by running
dig @127.0.0.1 _acme-challenge.example.com SOA
. If it comes
back without an ANSWER SECTION
, your DNS config is botched
and you need to get it to work first. Once it works locally, also be
sure to check it remotely, because there might be a firewall rule
necessary to allow incoming DNS traffic.
Certbot configuration
Certbot needs a configuration file to tell it how to reach the DNS
server and how to authenticate to it. Since there does not seem to be a
default location for that file, I placed it at
/etc/bind/certbot-credentials.ini
:
# Target DNS server
dns_rfc2136_server = 127.0.0.1
# Target DNS port
dns_rfc2136_port = 53
# TSIG key name
dns_rfc2136_name = certbot.
# TSIG key secret
dns_rfc2136_secret = here goes the secret from the .key file
# TSIG key algorithm
dns_rfc2136_algorithm = HMAC-SHA512
Be sure to install the dns-rfc2136
Plugin:
apt-get install python3-certbot-dns-rfc2136
With any luck, you should now be able to request a certificate using:
certbot certonly --dns-rfc2136 --dns-rfc2136-credentials /etc/bind/certbot-credentials.ini -d 'example.com' -d '*.example.com'
Congratulations!
Obtaining a Cert for a LAN
This method also works for obtaining a certificate for a host that
sits in a LAN and is not reachable by the outside world, as long as it
can talk to your DNS server. To try this, delegate the
_acme-challenge
name for a subdomain such as
_acme-challenge.lan.example.com
, and then try to obtain
certs for lan.example.com
and
*.lan.example.com
. The rest of lan.example.com
does not have to be visible to the outside world.