Let's Encrypt Wildcard Certificates with a Hidden Primary
Get wildcard certificates with the ACME DNS-01 challenge: certbot or acme.sh write the TXT record to your hidden primary with RFC 2136, and the secondary publishes it within seconds.
How DNS-01 works with a hidden primary
For a wildcard certificate such as *.example.ch, Let's Encrypt requires the DNS-01 challenge.
Your ACME client creates a TXT record _acme-challenge.example.ch, and Let's Encrypt looks it up
on your public nameservers. With a hidden primary, that works in four steps:
- The ACME client sends a signed dynamic update (RFC 2136) to your hidden primary.
- The primary adds the TXT record, raises the serial, and sends a NOTIFY.
- The secondary transfers the change and serves it on all public nameservers.
- Let's Encrypt queries the public nameservers and finds the record.
The ACME client never talks to the secondary. You need no API key at a DNS provider, only a TSIG key on your own primary.
1. Create a key for the ACME client
Use a separate key that may change only the challenge record, not a key that can transfer the zone.
2. Allow the update on the primary (BIND)
update-policy limits the key to TXT records at one name. With this rule, a stolen key cannot
change anything else in your zone.
BIND writes dynamic updates to a journal file next to the zone file, so the directory must be writable by
BIND. That is why the example uses /var/lib/bind.
Important: once a zone accepts dynamic updates, rndc reload no longer reads a
zone file you edited by hand, and it still reports success. For manual edits, use
rndc freeze example.ch, edit the file, then rndc thaw example.ch.
3a. certbot
Install the RFC 2136 plugin (on Debian and Ubuntu: python3-certbot-dns-rfc2136) and create a credentials file:
3b. acme.sh
acme.sh saves these values and uses them again for every renewal.
Why wait 60 seconds
Let's Encrypt checks the record from several places and may ask any of your public nameservers. The record has to be on all of them before the check starts. With NOTIFY, a secondary usually has the change within a few seconds; the 60 seconds are a safety margin. If a validation fails with "no TXT record found", check that your primary sends NOTIFY to the secondary. Without it, the secondary only sees the change at the next refresh.
Check it yourself while the challenge is running:
Option: a separate zone only for challenges
If you would rather not make your main zone dynamic, point the challenge name to a small zone that exists only for this purpose. Let's Encrypt follows the CNAME:
Make acme.example.ch its own zone with the update-policy rule. Add it on your
secondary like any other zone, and delegate it: put NS records for acme.example.ch into
example.ch that name the secondary's nameservers. Then point certbot or acme.sh at
acme.example.ch. Your main zone stays static, and you edit it as before.
Next Steps
- BIND as a hidden primary — transfer and notify settings
- Zone transfer errors — when a change does not arrive
- TSIG Setup