Let’s Encrypt wildcard certificate via certbot and RFC2136 with BIND

In the second part of our BIND & friends tutorial (cf part 1: Back to BIND)  we will focus on Dynamic DNS updates (RFC 2136) for BIND sub-domains, and how to:

  • get new DNS records added dynamically
  • obtain/renew Let’s Encrypt wildcard certificate without manual intervention

Is this just fantasy?

Aside from the obvious gain here, enabling Dynamic DNS updates has some major advantages as well as some drawbacks (at least on BIND). A quick sum up:

  • Cool beans
    • We can update from anywhere to add new records on-the-fly with any tool implementing RFC2136.
    • Makes including DNS steps into fully automated processes possible
    • Could allow full or sub-zone management via applications APIs hitting the DNS server for updates.
    • Allow you to validate Let’s Encrypt® wildcard certificate requests using the certbot client.
  • Danger zone
    • Your zone management is now ‘open’ to the world, restricted only by network rules and specific TSIG key (de-facto less secure than a single zone file accessible only locally by the root user).
    • As with anything dynamic, if things break for whichever reason it may take some time to notice (trusted monitoring solution is gold here).
    • Activating dynamic updates on a zone will lead to BIND overwriting your configuration, creating lots of $ORIGIN and dumping all your includes into the main file. Once a zone becomes open to dynamic updates, you should avoid doing any manual updates.

For all of these reasons, while the final choice is entirely up to you, we decided to activate dynamic DNS updates, but only on sub-zones that are subject to TLS wildcard certificates.

That way our main zones stays closed and formatted the way we like, while a whole sub-domain is entirely set up for dynamic updates and automatic renewal of wildcard certificates is enabled.

 Preparing your sub-zone

First off, open a second session to keep an eye on what is happening on BIND and OpenDNSSEC logs:

journalct -fu bind9 -fu opendnssec-signer

OpenDNSSEC

The sub-domain zone should also be set in OpenDNSSEC to reflect our BIND configuration.
Edit /etc/opendnssec/zonelist.xml and add the following:

<Zone name=”mysub.example.com”>
<Policy>lotus</Policy>
<SignerConfiguration>/var/lib/opendnssec/signconf/mysub.example.com.xml</SignerConfiguration>
<Adapters>
<Input>
<Adapter type=”DNS”>/etc/opendnssec/addns.xml</Adapter>
</Input>
<Output>
<Adapter type=”DNS”>/etc/opendnssec/addns.xml</Adapter>
</Output>
</Adapters>
</Zone>

Let’s import the new zone configuration:

ods-enforcer zonelist import

BIND

First off, let’s carve out a sub-zone out of our main domain: ‘mysub.example.com’. We need to use full delegated subzone for that, as virtual ones (declared inside the main domain zone file) will still result in BIND overwriting and taking over the whole zone file.

So let’s create our sub-zone file with some records, and declare the glue record inside of the main domain zone:

Sub-domain zone file /etc/bind/unsigned/mysub.example.com

$ORIGIN .
$TTL 3600 ; 1 hour
subzone.example.com IN SOA ns1.example.com. hostmaster.example.com. (
2018111017 ; serial
7200 ; refresh (2 hours)
900 ; retry (15 minutes)
1209600 ; expire (2 weeks)
86400 ; minimum (1 day)
)
NS ns1.example.com.
NS my.dns.provider.slave.record.
A X.X.X.X
$ORIGIN mysub.example.com.

On the main zone file:

; subzone hosted on the same name server
subzone.example.com. IN NS ns1.example.com.

Next step is to create another TSIG key that will be allowed to update sub-zone subzone.example.com.

dnssec-keygen -a HMAC-SHA512 -b 512 -n HOST certbot
cat Kcertbot.*.private
[output]
Private-key-format: v1.3
Algorithm: 165 (HMAC_SHA512)
Key: 7L7pVuohJaH7vDoDoURj4h55HLHfICDto318CgJHycVS7Xx1gd52njHCZRl+TIGLFbzVvKsfe9LjPzJUXqyKPg==
Bits: AAA=
Created: 20181116093106
Publish: 20181116093106
Activate: 20181116093106

Now we set up the configuration for our sub-domain as part of the previously define unsigned view in /etc/bind/named.conf.local:

[..]
// ####### Keys #########
key certbot {
algorithm hmac-sha512;
secret "7L7pVuohJaH7vDoDoURj4h55HLHfICDto318CgJHycVS7Xx1gd52njHCZRl+TIGLFbzVvKsfe9LjPzJUXqyKPg==";
};
[..]
// ####### ACLs #########
acl unsigned {
key opendnssec-in;
key certbot;
};
[..]
// ####### VIEWS #########
view unsigned {
[..]

zone subzone.example.com {
type master;
file "/etc/bind/unsigned/subzone.example.com";

// allow transfers to opendnssec using its key.
allow-transfer {
key "opendnssec-in";
};
// notify opendnssec which listens on port 51.
also-notify {
::1 port 51;
};
// authorizing certbot key to write TXT record for ACME challenge only
update-policy {
grant certbot-ddns name _acme-challenge.subzone.example.com. TXT;
};
};
[..]

// ############
// Zones
// ############
zone subzone.example.com {
type slave;
file "/etc/bind/signed/db.subzone.example.com";

masters {
::1 port 51 key opendnssec-out;
};
};
[..]

Now that all should be ready, let’s reload the configuration and check your log output while reloading BIND:

rndc reconfig

If you have any doubts / errors, restart BIND via systemctl restart bind9 before troubleshooting further.

Testing our solution

Using nsupdate

First off, let’s try adding a test ACME challenge record on our sub-domain using BIND tool nsupdate. We create file testing-nsupdate.txt that will contain all nsupdate commands:

cat testing-nsupdate.txt
[output]
server localhost 53
debug
zone subzone.example.com
update add _acme-challenge.subzone.example.com. 180 TXT "my-first-dns-dynamic-update"
show
send

Using previously created key file for certbot TSIG key, we run:

nsupdate -k Kcertbot-ddns.*.key -v testing-nsupdate.txt

If all is well, you should see something similar to:

;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id: 0
;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
;; ZONE SECTION:
;subzone.example.com. IN SOA

;; UPDATE SECTION:
_acme-challenge.subzone.example.com. 180 IN TXT “my-first-dynamic-dns-update”

Sending update to ::1#5353
Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id: 16698
;; flags:; ZONE: 1, PREREQ: 0, UPDATE: 1, ADDITIONAL: 1
;; ZONE SECTION:
;subzone.example.com. IN SOA

;; UPDATE SECTION:
_acme-challenge.subzone.example.com. 180 IN TXT “my-first-dynamic-dns-update”

;; TSIG PSEUDOSECTION:
certbot. 0 ANY TSIG hmac-sha512. 1542365148 300 64 SgnLBikgVrgHngM6QWe6Z/pujxCDpFJjcNGPYm/JpygZffb9WGRBr26j uImjUYuwfmuaCcn5z0fa+s0nj6g/CQ== 16698 NOERROR 0

Reply from update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id: 16698
;; flags: qr ra; ZONE: 1, PREREQ: 0, UPDATE: 0, ADDITIONAL: 1
;; ZONE SECTION:
;subzone.example.com. IN SOA

;; TSIG PSEUDOSECTION:
certbot. 0 ANY TSIG hmac-sha512. 1542365148 300 64 qzfU+ors1MwzFFrkBZQVflsSnIIvtrOqQwZ/N9IzePUfJPfr+1/TEdws 3NaM+kAWyNYy06NCFCEf2dPPQhEjOA== 16698 NOERROR 0

The logs should also report the successful update and the different notifications and transfers:

Nov 16 11:13:29 ns1.example.com named[8974]: client ::1#53622/key certbot: view unsigned: updating zone 'subzone.example.com/IN': adding an RR at '_acme-challenge.subzone.example.com' TXT "my-first-dynamic-dns-update"
Nov 16 11:13:29 ns1.example.com named[8974]: zone subzone.example.com/IN/unsigned: sending notifies (serial 2018111020)
Nov 16 11:13:29 ns1.example.com named[8974]: network unreachable resolving 'ns6.gandi.net/AAAA/IN': 2001:4b98:d:1::39#53
Nov 16 11:13:29 ns1.example.com ods-signerd[24857]: [xfrd] zone subzone.example.com request udp/ixfr=2018111019 to ::1
Nov 16 11:13:29 ns1.example.com ods-signerd[24857]: [xfrd] zone subzone.example.com received too short udp reply from ::1, retry tcp
Nov 16 11:13:29 ns1.example.com ods-signerd[24857]: [xfrd] zone subzone.example.com request tcp/ixfr=2018111019 to ::1
Nov 16 11:13:29 ns1.example.com named[8974]: client ::1#56197/key opendnssec-in (subzone.example.com): view unsigned: transfer of 'subzone.example.com/IN': IXFR started: TSIG opendnssec-in (serial 2018111019 -> 2018111020)
Nov 16 11:13:29 ns1.example.com named[8974]: client ::1#56197/key opendnssec-in (subzone.example.com): view unsigned: transfer of 'subzone.example.com/IN': IXFR ended
Nov 16 11:13:29 ns1.example.com ods-signerd[24857]: [xfrd] zone subzone.example.com transfer done [notify acquired 1542366809, serial on disk 2018111020, notify serial 2018111020]
Nov 16 11:13:29 ns1.example.com ods-signerd[24857]: [STATS] subzone.example.com 2018111602 RR[count=1 time=0(sec)] NSEC3[count=2 time=0(sec)] RRSIG[new=4 reused=16 time=0(sec) avg=0(sig/sec)] TOTAL[time=0(sec)]
Nov 16 11:13:29 ns1.example.com named[8974]: client ::1#44800: view default: received notify for zone 'subzone.example.com'
Nov 16 11:13:29 ns1.example.com named[8974]: zone subzone.example.com/IN/default: notify from ::1#44800: serial 2018111602
Nov 16 11:13:29 ns1.example.com named[8974]: zone subzone.example.com/IN/default: Transfer started.
Nov 16 11:13:29 ns1.example.com named[8974]: transfer of 'subzone.example.com/IN/default' from ::1#51: connected using ::1#51969
Nov 16 11:13:29 ns1.example.com named[8974]: zone subzone.example.com/IN/default: transferred serial 2018111602: TSIG 'opendnssec-out'
Nov 16 11:13:29 ns1.example.com named[8974]: transfer of 'subzone.example.com/IN/default' from ::1#51: Transfer status: success
Nov 16 11:13:29 ns1.example.com named[8974]: transfer of 'subzone.example.com/IN/default' from ::1#51: Transfer completed: 1 messages, 14 records, 2866 bytes, 0.082 secs (34951 bytes/sec)
Nov 16 11:13:29 ns1.example.com named[8974]: zone subzone.example.com/IN/default: sending notifies (serial 2018111602)
Nov 16 11:13:29 ns1.example.com named[8974]: network unreachable resolving 'ns6.gandi.net/AAAA/IN': 2400:cb00:2049:1::a29f:186f#53
Nov 16 11:13:29 ns1.example.com named[8974]: client 172.X.X.X#35240/key nsd-slave (subzone.example.com): view default: transfer of 'subzone.example.com/IN': AXFR started: TSIG nsd-slave (serial 2018111602)
Nov 16 11:13:29 ns1.example.com named[8974]: client 172.X.X.X#35240/key nsd-slave (subzone.example.com): view default: transfer of 'subzone.example.com/IN': AXFR ended

Using certbot

Last but not least, move your certbot.* keys to the machine that will be used to request the wildcard sub-domain certificate (typically your reverse-proxy server) .

  • certbot package should be installed and up-to-date.

Before running the final test to automatically obtain a wildcard certificate for *.subzone.example.com, prepare an ini file /etc/letsencrypt/rfc2136.ini that will be used to define settings needed to get RFC2136 updates working through certbot client.

# Target DNS server | Your BIND server
dns_rfc2136_server = 192.168.X.X
# Target DNS port
dns_rfc2136_port = 53
# TSIG key name
dns_rfc2136_name = certbot
# TSIG key secret
dns_rfc2136_secret = 7L7pVuohJaH7vDoDoURj4h55HLHfICDto318CgJHycVS7Xx1gd52njHCZRl+TIGLFbzVvKsfe9LjPzJUXqyKPg==
# TSIG key algorithm
dns_rfc2136_algorithm = HMAC-SHA512

Let’s do the final testing:

/usr/bin/certbot -n --dns-rfc2136 --dns-rfc2136-credentials=/etc/letsencrypt/rfc2136.ini --dns-rfc2136-propagation-seconds 30 --logs-dir /var/log/letsencrypt --work-dir
/etc/letsencrypt/live --config-dir /etc/letsencrypt -m hostmaster@example.com --rsa-key-size 3072 -d *.subzone.example.com

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator dns-rfc2136, Installer None
Renewing an existing certificate
Performing the following challenges:
dns-01 challenge for subzone.example.com
Waiting 30 seconds for DNS changes to propagate
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/subzone.example.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/subzone.example.com/privkey.pem
Your cert will expire on 2019-02-08. To obtain a new or tweaked
version of this certificate in the future, simply run certbot
again. To non-interactively renew *all* of your certificates, run
"certbot renew"
- If you like Certbot, please consider supporting our work by:

Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le

Certificate and key for subzone.example.com have been renewed!

That’s it!

Leave a Reply

Your email address will not be published. Required fields are marked *