Files
jarl/docs/acme.md
T
abrown38andClaude Opus 5.5 bd6b8778e4 Jarl 0.1.0: manage BIND through a web UI, REST API and CLI
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>
2026-10-07 15:03:13 +00:00

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
  • 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

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-cname to 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.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.