diff --git a/CHANGELOG.md b/CHANGELOG.md index 77c73a8..ad39c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 enclosing /24's zone. - 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 name servers (and when glue addresses are needed), how reverse zones map to networks, and why dynamic updates give way to API tokens. diff --git a/Makefile b/Makefile index 5d27cd5..bf17e98 100644 --- a/Makefile +++ b/Makefile @@ -48,9 +48,10 @@ install: build 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 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 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 \ install -d -m 0750 $(DESTDIR)/etc/jarl; \ bin/jarld config --example > $(DESTDIR)/etc/jarl/jarl.yaml; \ diff --git a/README.md b/README.md index d908a37..d5a86ee 100644 --- a/README.md +++ b/README.md @@ -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, and exportable as JSON. Stored as aggregates in a separate database with 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. * **Adopts an existing server**: imports named.conf and zone files (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/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/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/validation.md](docs/validation.md) | Every check Jarl makes | | [docs/operations.md](docs/operations.md) | Backups, drift, failures, recovery, monitoring | diff --git a/contrib/lego/jarl-lego.sh b/contrib/lego/jarl-lego.sh new file mode 100755 index 0000000..12784fd --- /dev/null +++ b/contrib/lego/jarl-lego.sh @@ -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" diff --git a/docs/acme.md b/docs/acme.md index 0196008..87b89f4 100644 --- a/docs/acme.md +++ b/docs/acme.md @@ -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. 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 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 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 the main zone is never modified by automation. diff --git a/docs/development.md b/docs/development.md index d99a742..4b429a7 100644 --- a/docs/development.md +++ b/docs/development.md @@ -26,6 +26,7 @@ internal/ config/ jarld's YAML configuration deploy/ systemd unit, sysusers.d and tmpfiles.d files 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 ```