Jarl keeps a BIND 9.20 server's whole configuration (views, ACLs, TSIG keys, zones of every type, records, named.conf options) in SQLite and renders named.conf, keys.conf and zone files from it. Every change goes through one changeset pipeline: whole-configuration validation, named-checkconf -z and named-checkzone in a staging directory, atomic install, rndc reconfig/reload, serial verification with rndc zonestatus, and rollback if named does not load the change. Changes apply one at a time by default or are bundled and applied together. Applied changesets can be reverted. - jarld: the daemon with the REST API and an embedded web UI, plus import, adopt, check, resync, backup and local user recovery commands - jarl: the CLI, including ACME DNS-01 hooks for certbot and acme.sh - users with argon2id passwords and CSRF-protected sessions; hashed API tokens with granular grants and roles; an audit log - systemd unit, sysusers.d/tmpfiles.d files, Makefile - unit tests, integration tests against a real named, scripts/e2e.sh - docs/ guides, README and CHANGELOG Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.7 KiB
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.<domain>. 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
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:
jarl user create certbot --no-password --grant "records:write zones=adbraun.lol names=_acme-challenge.* types=TXT"
2. What jarl acme does
jarl acme set DOMAIN VALUE... # publish
jarl acme unset DOMAIN [VALUE...] # remove these values, or all of them
DOMAINis 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 NAMElimits 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
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:
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:
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-cnameto both hooks, e.g.jarl --token @/etc/jarl/acme.token acme certbot-auth --follow-cname. jarl looks up_acme-challenge.<domain>and writes the TXT record at the target (www.acme.adbraun.lol). The lookup uses the host's resolvers. Add--resolver 127.0.0.1to 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.lolputs 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.lolwrites 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 withjarl --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 thatallow-querylets the internet query the zone. jarl audit --user certbotshows every attempt.