diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d8d965..3b5dc57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ All notable changes to Jarl are documented here. The format follows ## [Unreleased] +### Added + +- A docs/acme.md section: lego on a host inside a split DNS fails its own + propagation check when the challenge is written into the external view + only (`JARL_VIEW`, a one-view token, `.../httpreq/view/...`): why, and + the ways round it (no view limit, `--dns.propagation.disable-ans` or + `--dns.propagation.wait`, Traefik's `propagation.disableANSChecks`). +- docs/acme.md and `jarl-lego` show lego 5's commands (`lego run ...`, + which also renews; `lego migrate` when upgrading), with lego 4's + beside them. Jarl's side is the same for both. + ## [0.6.2] - 2026-10-21 ### Added diff --git a/contrib/lego/jarl-lego.sh b/contrib/lego/jarl-lego.sh index 12784fd..a9b2dd0 100755 --- a/contrib/lego/jarl-lego.sh +++ b/contrib/lego/jarl-lego.sh @@ -7,12 +7,17 @@ # # 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 +# lego run --accept-tos --email you@example.com --dns exec -d example.com -d '*.example.com' +# (lego 4: lego --accept-tos --email ... --dns exec -d ... run, and "renew" to renew) # # 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) +# On a host inside a split DNS, lego's own propagation check then can't +# see the record: leave it unset, or run lego with +# --dns.propagation.disable-ans (lego 5; --dns.propagation-disable-ans in lego 4) +# (docs/acme.md, "Split DNS and lego's propagation check"). # JARL_BIN Path of the jarl binary (optional; default "jarl") JARL_BIN="${JARL_BIN:-jarl}" diff --git a/docs/acme.md b/docs/acme.md index 423d816..7dd0082 100644 --- a/docs/acme.md +++ b/docs/acme.md @@ -98,10 +98,23 @@ sudo install -m 0755 contrib/lego/jarl-lego.sh /usr/local/bin/jarl-lego # or f 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 +lego run --accept-tos --email you@adbraun.lol --dns exec -d adbraun.lol -d '*.adbraun.lol' ``` -Renew with the same variables and `lego ... renew` in place of `run`. +That is lego 5. `lego run` also renews: run the same command daily (a +cron job or systemd timer), and it renews once a third of the +certificate's lifetime is left. **lego 4** takes its options before the +command, and renews with `renew`: + +```sh +lego --accept-tos --email you@adbraun.lol --dns exec -d adbraun.lol -d '*.adbraun.lol' run # first time +lego --email you@adbraun.lol --dns exec -d adbraun.lol -d '*.adbraun.lol' renew # then daily +``` + +Upgrading from lego 4 to 5, run `lego migrate` once (with `--path` if you +keep lego's data somewhere other than `./.lego`) before anything else: +lego 5 stores accounts and certificates differently. The exec and httpreq +providers, and so Jarl's side, are the same in both. * 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 @@ -114,7 +127,66 @@ Renew with the same variables and `lego ... renew` in place of `run`. * 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. + once named serves the record, so this check passes on its first try, + as long as the view that answers lego's host has the record: with + `JARL_VIEW` on a host inside your network it doesn't. See + [Split DNS and lego's propagation check](#split-dns-and-legos-propagation-check). + +### Split DNS and lego's propagation check + +Before lego asks the CA to look, it checks the record itself, asking the +zone's name servers (`ns1.adbraun.lol`) directly. With views, they answer +lego's host from the view that matches *it*. On a host inside your +network that is the internal view. If the challenge was written into +another view only (`JARL_VIEW=external`, `--view external`, a token +limited to `views=external`, or the httpreq endpoint +`.../httpreq/view/external`), lego never sees it, and gives up after +its propagation timeout without the CA having looked: + +``` +propagation: time limit exceeded: last error: authoritative nameservers: +NS ns1.adbraun.lol.:53 returned NXDOMAIN for _acme-challenge.stats.adbraun.lol. +``` + +A name whose authorization is still valid from an earlier run ("authorization +already valid; skipping challenge") isn't checked at all, so one name can +work and the next fail with the same settings. + +The ways round it, best first: + +1. **Don't limit the view.** Leave `JARL_VIEW` unset (and give the token + no `views=` filter, the httpreq endpoint no `/view/...`): the record + goes into every view that has the zone, the internal one too, and + lego's check passes at once. The challenge is a random value that is + useless once the CA has checked it, and it is removed straight after. +2. **Skip lego's check of the name servers.** The record is already + served when Jarl answers, so lego's check adds nothing: + + ```sh + lego run --dns exec --dns.propagation.disable-ans ... # lego 5 + lego --dns exec --dns.propagation-disable-ans ... run # lego 4.20 to 4.x + lego --dns exec --dns.disable-cp ... run # older lego 4 + ``` + + or replace the checks with a fixed wait, which also gives secondary + name servers time to catch up: `--dns.propagation.wait 30s` (lego 5; + `--dns.propagation-wait 30s` in lego 4.20 and later). In lego 5 these + can also be set in the environment: `LEGO_DNS_PROPAGATION_DISABLE_ANS=true`, + `LEGO_DNS_PROPAGATION_WAIT=30s`. +3. **In Traefik**, the same under `dnsChallenge` (Traefik 3.2 and later; + older versions have `disablePropagationCheck: true`): + + ```yaml + dnsChallenge: + provider: httpreq + propagation: + disableANSChecks: true # or delayBeforeChecks: 30s + ``` +4. **Run lego where the external view answers it**, such as a host outside + your network. Rarely the easy option. + +certbot (section 3) doesn't check by itself, and acme.sh (section 4) +checks through public DNS-over-HTTPS resolvers, so neither runs into this. ## 6. lego and Traefik over HTTP (httpreq), without the jarl CLI @@ -299,14 +371,20 @@ of the `acme` role. export HTTPREQ_ENDPOINT=https://ns1.adbraun.lol:8053/api/v1/acme/httpreq export HTTPREQ_USERNAME=lego export HTTPREQ_PASSWORD_FILE=/etc/jarl/lego.token # or HTTPREQ_PASSWORD=jarl_... -lego --email you@adbraun.lol --dns httpreq -d adbraun.lol -d '*.adbraun.lol' run +lego run --accept-tos --email you@adbraun.lol --dns httpreq -d adbraun.lol -d '*.adbraun.lol' ``` +That is lego 5; in lego 4 the options go before the command (`lego +--email ... --dns httpreq -d ... run`, and `renew` to renew). See +[section 5](#5-lego). + Leave `HTTPREQ_MODE` unset (its `RAW` mode sends a different body, which jarld refuses with a message saying so). If jarld's certificate doesn't come from a public CA, lego has to trust yours: `SSL_CERT_FILE=/path/to/your-ca.pem` (lego's `LEGO_CA_CERTIFICATES` covers only the ACME server). To write into one view only, put -it in the endpoint: `.../api/v1/acme/httpreq/view/external`. +it in the endpoint: `.../api/v1/acme/httpreq/view/external`; on a host inside +your network, read [Split DNS and lego's propagation check](#split-dns-and-legos-propagation-check) +first. ### 4. Traefik @@ -412,4 +490,8 @@ the main zone is never modified by automation. `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. +* lego says `propagation: time limit exceeded ... returned NXDOMAIN for + _acme-challenge...`: the record is in a view that doesn't answer lego's + host, usually `JARL_VIEW=external` on a host inside your network. See + [Split DNS and lego's propagation check](#split-dns-and-legos-propagation-check). * `jarl audit --user certbot` shows every attempt.