Add a program for lego's exec DNS provider

contrib/lego/jarl-lego.sh turns lego's "present|cleanup FQDN VALUE" calls
into jarl acme set|unset --exact, so lego (and CNAME delegation to a
dedicated ACME zone) works with Jarl. docs/acme.md explains the setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-08 13:30:43 +00:00
1 parent 5cb8602a7f
commit 0221c78c8a
6 files changed
+88 -4

No files matched your search

+3
View File
@@ -24,6 +24,9 @@ The first version, to be released as 0.1.0.
the web UI. Networks smaller than a /24 explain that they belong in the the web UI. Networks smaller than a /24 explain that they belong in the
enclosing /24's zone. enclosing /24's zone.
- docs/operations.md: setting up a secondary server with its own jarld. - docs/operations.md: setting up a secondary server with its own jarld.
- lego support: `contrib/lego/jarl-lego.sh` is a program for lego's `exec`
DNS provider that publishes DNS-01 challenges through `jarl acme`,
including challenge names reached through a CNAME (docs/acme.md).
- docs/concepts.md: what each zone type does, what to enter as a new zone's - docs/concepts.md: what each zone type does, what to enter as a new zone's
name servers (and when glue addresses are needed), how reverse zones map name servers (and when glue addresses are needed), how reverse zones map
to networks, and why dynamic updates give way to API tokens. to networks, and why dynamic updates give way to API tokens.
+2 -1
View File
@@ -48,9 +48,10 @@ install: build
install -D -m 0644 deploy/tmpfiles.d/jarl.conf $(DESTDIR)$(TMPFILESDIR)/jarl.conf install -D -m 0644 deploy/tmpfiles.d/jarl.conf $(DESTDIR)$(TMPFILESDIR)/jarl.conf
install -D -m 0644 README.md $(DESTDIR)$(DOCDIR)/README.md install -D -m 0644 README.md $(DESTDIR)$(DOCDIR)/README.md
install -D -m 0644 CHANGELOG.md $(DESTDIR)$(DOCDIR)/CHANGELOG.md install -D -m 0644 CHANGELOG.md $(DESTDIR)$(DOCDIR)/CHANGELOG.md
install -d $(DESTDIR)$(DOCDIR)/docs $(DESTDIR)$(DOCDIR)/contrib/acme.sh install -d $(DESTDIR)$(DOCDIR)/docs $(DESTDIR)$(DOCDIR)/contrib/acme.sh $(DESTDIR)$(DOCDIR)/contrib/lego
install -m 0644 docs/*.md $(DESTDIR)$(DOCDIR)/docs/ install -m 0644 docs/*.md $(DESTDIR)$(DOCDIR)/docs/
install -m 0644 contrib/acme.sh/dns_jarl.sh $(DESTDIR)$(DOCDIR)/contrib/acme.sh/ install -m 0644 contrib/acme.sh/dns_jarl.sh $(DESTDIR)$(DOCDIR)/contrib/acme.sh/
install -m 0755 contrib/lego/jarl-lego.sh $(DESTDIR)$(DOCDIR)/contrib/lego/
@if [ ! -e $(DESTDIR)/etc/jarl/jarl.yaml ]; then \ @if [ ! -e $(DESTDIR)/etc/jarl/jarl.yaml ]; then \
install -d -m 0750 $(DESTDIR)/etc/jarl; \ install -d -m 0750 $(DESTDIR)/etc/jarl; \
bin/jarld config --example > $(DESTDIR)/etc/jarl/jarl.yaml; \ bin/jarld config --example > $(DESTDIR)/etc/jarl/jarl.yaml; \
+2 -2
View File
@@ -46,7 +46,7 @@ error: www.adbraun.lol has a CNAME, so it can't have any other records (found A)
queries, record types, cache hit rate. Shown in the web UI and the CLI, queries, record types, cache hit rate. Shown in the web UI and the CLI,
and exportable as JSON. Stored as aggregates in a separate database with and exportable as JSON. Stored as aggregates in a separate database with
retention you set. retention you set.
* **Let's Encrypt DNS-01** built in: `jarl acme` plus certbot and acme.sh * **Let's Encrypt DNS-01** built in: `jarl acme` plus certbot, acme.sh and lego
hooks, including CNAME delegation to a dedicated ACME zone. hooks, including CNAME delegation to a dedicated ACME zone.
* **Adopts an existing server**: imports named.conf and zone files * **Adopts an existing server**: imports named.conf and zone files
(comments included), then switches named over with an automatic (comments included), then switches named over with an automatic
@@ -88,7 +88,7 @@ in `/etc/jarl/jarl.yaml` to reach it from your management network.
| [docs/api.md](docs/api.md) | REST API reference | | [docs/api.md](docs/api.md) | REST API reference |
| [docs/permissions.md](docs/permissions.md) | Users, tokens, grants and roles | | [docs/permissions.md](docs/permissions.md) | Users, tokens, grants and roles |
| [docs/views.md](docs/views.md) | Split DNS: adding views to a server that has none | | [docs/views.md](docs/views.md) | Split DNS: adding views to a server that has none |
| [docs/acme.md](docs/acme.md) | Let's Encrypt DNS-01 with certbot and acme.sh | | [docs/acme.md](docs/acme.md) | Let's Encrypt DNS-01 with certbot, acme.sh and lego |
| [docs/stats.md](docs/stats.md) | Query statistics: enabling, what is collected, retention | | [docs/stats.md](docs/stats.md) | Query statistics: enabling, what is collected, retention |
| [docs/validation.md](docs/validation.md) | Every check Jarl makes | | [docs/validation.md](docs/validation.md) | Every check Jarl makes |
| [docs/operations.md](docs/operations.md) | Backups, drift, failures, recovery, monitoring | | [docs/operations.md](docs/operations.md) | Backups, drift, failures, recovery, monitoring |
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env sh
# Jarl (BIND manager): a program for lego's "exec" DNS provider.
#
# lego runs it as "jarl-lego present FQDN VALUE" and "jarl-lego cleanup
# FQDN VALUE", where FQDN is the challenge name after lego has followed
# any CNAME. It publishes and removes the TXT record through the jarl CLI.
#
# Install it, then:
# EXEC_PATH=/usr/local/bin/jarl-lego JARL_TOKEN=@/etc/jarl/lego.token \
# lego --email you@example.com --dns exec -d example.com -d '*.example.com' run
#
# Options (environment):
# JARL_SERVER Jarl server URL, e.g. https://ns1.example.com:8053 (optional if the jarl CLI is configured)
# JARL_TOKEN API token, or @FILE (optional if the jarl CLI is configured)
# JARL_VIEW Only write into this view (optional; default every view that has the zone)
# JARL_BIN Path of the jarl binary (optional; default "jarl")
JARL_BIN="${JARL_BIN:-jarl}"
if [ "$2" = "--" ]; then
echo "jarl-lego: EXEC_MODE=RAW is not supported; unset EXEC_MODE" >&2
exit 2
fi
case "$1" in
present) action="set" ;;
cleanup) action="unset" ;;
*)
echo "usage: jarl-lego present|cleanup FQDN VALUE" >&2
exit 2
;;
esac
if [ $# -ne 3 ]; then
echo "usage: jarl-lego present|cleanup FQDN VALUE" >&2
exit 2
fi
# lego passes the full record name, so --exact keeps jarl from adding
# _acme-challenge. again.
if [ -n "$JARL_VIEW" ]; then
exec "$JARL_BIN" -q acme "$action" --exact --view "$JARL_VIEW" "$2" "$3"
fi
exec "$JARL_BIN" -q acme "$action" --exact "$2" "$3"
+36 -1
View File
@@ -79,7 +79,36 @@ remembers `JARL_SERVER`, `JARL_TOKEN`, `JARL_VIEW` (optional) and `JARL_BIN`
(path to jarl, optional) in acme.sh's account configuration for renewals. (path to jarl, optional) in acme.sh's account configuration for renewals.
When they are unset, jarl's own configuration file is used. When they are unset, jarl's own configuration file is used.
## 5. A dedicated ACME zone (CNAME delegation) ## 5. lego
lego's `exec` provider runs a program to publish and remove the record.
Jarl ships one, `contrib/lego/jarl-lego.sh`, which calls the `jarl` CLI
(so jarl must be installed on the same host):
```sh
sudo install -m 0755 contrib/lego/jarl-lego.sh /usr/local/bin/jarl-lego # or from /usr/local/share/doc/jarl/contrib/
export EXEC_PATH=/usr/local/bin/jarl-lego
export JARL_SERVER=https://ns1.adbraun.lol:8053
export JARL_TOKEN=@/etc/jarl/certbot.token
lego --email you@adbraun.lol --dns exec -d adbraun.lol -d '*.adbraun.lol' run
```
Renew with the same variables and `lego ... renew` in place of `run`.
* lego runs `jarl-lego present FQDN VALUE` and `jarl-lego cleanup FQDN VALUE`,
and the script passes them to `jarl acme set|unset --exact`. FQDN is the
full record name (`_acme-challenge.adbraun.lol.`), so `--exact` stops
jarl from adding `_acme-challenge.` a second time.
* `JARL_TOKEN` may be the token itself or `@FILE`. `JARL_VIEW` limits the
record to one view, and `JARL_BIN` points at jarl if it isn't in `PATH`.
When `JARL_SERVER` and `JARL_TOKEN` are unset, jarl's configuration file
is used.
* Leave `EXEC_MODE` unset: the script needs lego's default mode, not `RAW`.
* lego waits until the record is visible on the zone's name servers
(`EXEC_PROPAGATION_TIMEOUT`, 60 seconds by default). Jarl answers only
once named serves the record, so this check passes on its first try.
## 6. A dedicated ACME zone (CNAME delegation)
Instead of letting a token write into your main zone, point each challenge 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 name at a small zone that exists only for ACME. The token may then write
@@ -112,6 +141,12 @@ target:
The hook passes acme.sh's full record name to `jarl acme --exact`, so both The hook passes acme.sh's full record name to `jarl acme --exact`, so both
modes work. modes work.
* **lego** follows the CNAME itself (unless `LEGO_DISABLE_CNAME_SUPPORT`
is set) and passes the target to `jarl-lego`, which writes there with
`--exact`. The `acme` role allows only names that start with
`_acme-challenge`, so the token needs the grant above, not the role. lego
uses the host's resolvers for the lookup; `--dns.resolvers
ns1.adbraun.lol:53` asks the Jarl server instead.
A leaked ACME token can then only touch TXT records in the ACME zone, and A leaked ACME token can then only touch TXT records in the ACME zone, and
the main zone is never modified by automation. the main zone is never modified by automation.
+1
View File
@@ -26,6 +26,7 @@ internal/
config/ jarld's YAML configuration config/ jarld's YAML configuration
deploy/ systemd unit, sysusers.d and tmpfiles.d files deploy/ systemd unit, sysusers.d and tmpfiles.d files
contrib/acme.sh/ acme.sh DNS API hook contrib/acme.sh/ acme.sh DNS API hook
contrib/lego/ program for lego's exec DNS provider
scripts/e2e.sh end-to-end test against a real named scripts/e2e.sh end-to-end test against a real named
``` ```