# Let's Encrypt (ACME DNS-01) with Jarl The DNS-01 challenge proves that you control a domain by publishing a TXT record at `_acme-challenge.`. Jarl does this through its API, so no dynamic updates (`allow-update`, nsupdate) are needed. That matters because Jarl regenerates zone files and doesn't allow dynamic zones. **When a `jarl acme` command returns, named is already serving the record.** Jarl reloads the zone and checks the new serial with `rndc zonestatus` before it reports success, so hooks need no "sleep and hope" delay. If you also run secondary servers, they pick the change up through NOTIFY within seconds. Add a short `--dns-propagation-seconds`/`--dnssleep` only if your CA checks them too. ## 1. A token that can only publish challenges ```sh jarl user create certbot --no-password --role acme --comment "Let's Encrypt" jarl token create certbot --user certbot --role acme -q > certbot.token sudo install -m 0600 certbot.token /etc/jarl/certbot.token && rm certbot.token ``` The `acme` role is `records:write names=_acme-challenge.* types=TXT`. The token can add and remove challenge TXT records in any zone and nothing else. To restrict it to your zones, use an explicit grant instead: ```sh jarl user create certbot --no-password --grant "records:write zones=adbraun.lol names=_acme-challenge.* types=TXT" ``` ## 2. What `jarl acme` does ```sh jarl acme set DOMAIN VALUE... # publish jarl acme unset DOMAIN [VALUE...] # remove these values, or all of them ``` * `DOMAIN` is the name on the certificate. `_acme-challenge.` is added for you, and a leading `*.` is dropped, since a wildcard and its apex share one challenge name. A name that already starts with `_acme-challenge.` is used as it is. * The zone is found automatically: the longest matching primary zone. * If the zone exists in several **views** (split DNS), the record is written into every view, so the CA (on the internet, in your external view) and your internal resolvers see the same thing. `--view NAME` limits it to one. * All values and views go into one changeset. Setting a value that is already there succeeds, so a retried hook doesn't fail, and removing a value that isn't there succeeds as well. * The record TTL is 60 seconds (`--ttl`). ## 3. certbot ```sh sudo certbot certonly --manual --preferred-challenges dns \ --manual-auth-hook 'jarl --token @/etc/jarl/certbot.token acme certbot-auth' \ --manual-cleanup-hook 'jarl --token @/etc/jarl/certbot.token acme certbot-cleanup' \ -d adbraun.lol -d '*.adbraun.lol' ``` The hooks read `CERTBOT_DOMAIN` and `CERTBOT_VALIDATION` from certbot. Add `--server https://ns1.adbraun.lol:8053` when certbot runs on another host, or `--server http://127.0.0.1:8053` on the DNS server itself (root's jarl CLI has no configuration file). certbot stores the hooks in the renewal configuration, so `certbot renew` works unattended. ## 4. acme.sh Install the DNS API hook that comes with Jarl: ```sh cp contrib/acme.sh/dns_jarl.sh ~/.acme.sh/dnsapi/ # or from /usr/local/share/doc/jarl/contrib/ export JARL_SERVER=https://ns1.adbraun.lol:8053 export JARL_TOKEN=$(cat /etc/jarl/certbot.token) acme.sh --issue --dns dns_jarl -d adbraun.lol -d '*.adbraun.lol' ``` The hook calls the `jarl` CLI, which must be installed on the same host. It remembers `JARL_SERVER`, `JARL_TOKEN`, `JARL_VIEW` (optional) and `JARL_BIN` (path to jarl, optional) in acme.sh's account configuration for renewals. When they are unset, jarl's own configuration file is used. ## 5. A dedicated ACME zone (CNAME delegation) Instead of letting a token write into your main zone, point each challenge name at a small zone that exists only for ACME. The token may then write only there: ```sh jarl zone create acme.adbraun.lol --ns ns1.adbraun.lol. jarl record add adbraun.lol acme NS ns1.adbraun.lol. # delegate it jarl record add adbraun.lol _acme-challenge CNAME apex.acme.adbraun.lol. jarl record add adbraun.lol _acme-challenge.www CNAME www.acme.adbraun.lol. jarl user create acme --no-password --grant "records:write zones=acme.adbraun.lol types=TXT" jarl token create acme --user acme -q > acme.token ``` The CA follows the CNAME, so the TXT record has to be written at its target: * **certbot**: add `--follow-cname` to both hooks, e.g. `jarl --token @/etc/jarl/acme.token acme certbot-auth --follow-cname`. jarl looks up `_acme-challenge.` and writes the TXT record at the target (`www.acme.adbraun.lol`). The lookup uses the host's resolvers. Add `--resolver 127.0.0.1` to ask the Jarl server directly, for example when the host's resolver sees a different view. * **acme.sh** does the lookup itself: * `--challenge-alias acme.adbraun.lol` puts every TXT record at `_acme-challenge.acme.adbraun.lol`. Point all the CNAMEs there (`_acme-challenge.www CNAME _acme-challenge.acme.adbraun.lol.`). * `--domain-alias www.acme.adbraun.lol` writes at exactly that name. The hook passes acme.sh's full record name to `jarl acme --exact`, so both modes work. A leaked ACME token can then only touch TXT records in the ACME zone, and the main zone is never modified by automation. ## Troubleshooting * `no primary zone that you can see contains ...`: the token's grants don't cover the zone, or the zone isn't a primary zone in Jarl. * `permission denied: this needs records:write ...` (exit code 3): the record name or type isn't allowed for the token. Compare with `jarl --token @FILE whoami`. * The CA says the TXT record is missing: check from outside with `dig @ns1.adbraun.lol _acme-challenge.adbraun.lol TXT`. With views, make sure the record is in the view that matches the CA (it is by default) and that `allow-query` lets the internet query the zone. * `jarl audit --user certbot` shows every attempt.