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

131 lines
5.7 KiB
Markdown

# 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
```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.<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.