The web UI's certificate from Let's Encrypt, through Jarl

tls.acme names the certificate and jarl: the Jarl server and a token that
may only write the challenge record. Steward gets the certificate with
DNS-01 (lego), writing the record through Jarl's API into every view that
has the zone, renews it with a third of its lifetime left and swaps it
in without a restart. A check runs before every order: Jarl, the zone,
the token's permission, a test record, the zone's name servers, public
resolvers, clean-up; each step says what to fix. Staging by default.

With a certificate, each listener serves HTTPS and plain HTTP on one port
(redirecting plain HTTP, except from the server itself).

steward cert / cert check / cert renew, Server -> Certificate in the web
UI, GET /certificate, POST /certificate/check|renew. docs/certificates.md
and docs/jarl.md (the Jarl user and the token's grant).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-14 15:46:36 +01:00
1 parent d0ee2c8844
commit 7b3f0f764c
32 files changed
+2544 -27

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
# Connecting Steward to Jarl
Steward uses [Jarl](https://gitea.adbraun.lol/adam/jarl), the BIND
manager, for its DNS records. In this version that means one thing: the
DNS-01 challenge for the web UI's certificate ([certificates.md](certificates.md)).
Registering clients' names is planned to use the same connection.
Steward talks to Jarl's REST API with an API token. Give it a **user of its
own**, with **only the permission it needs**.
## 1. A user for Steward, in Jarl
On the Jarl server, make a user without a password. It can't log in to
Jarl's web UI, only use its tokens. Grant it the challenge record of
Steward's own name, and nothing else:
```sh
jarl user create steward --no-password --comment "Steward on dhcp01" \
--grant "records:write zones=adbraun.lol names=_acme-challenge.dhcp.adbraun.lol types=TXT"
```
Use your zone for `adbraun.lol`, and the name Steward's web UI is reached
by for `dhcp.adbraun.lol` (`tls.acme.domain`). The grant lets the token add
and remove TXT records at exactly `_acme-challenge.dhcp.adbraun.lol`:
* nothing else in the zone (`names=`);
* no other record type (`types=TXT`);
* no other zone (`zones=`).
It can see that the zone exists, which it needs to find it, but it can't
read the zone's other records.
Why not something wider:
* **Jarl's `acme` role** (`records:write names=_acme-challenge.* types=TXT`)
would let the token publish challenges for *any* name in *any* zone, and
so obtain certificates for all of them. Steward needs only its own name.
* **Your own account's token** would carry all your permissions, and
Steward's certificate would stop renewing the day your account changes.
A user per program keeps the two apart, and the audit log shows
`steward/...` for what Steward did.
If the challenge name is a CNAME into a zone kept for challenges, such as
`_acme-challenge.dhcp.adbraun.lol CNAME dhcp.acme.adbraun.lol.`, grant the
target instead: `zones=acme.adbraun.lol names=dhcp.acme.adbraun.lol`.
## 2. The token
Make the token, and copy it to the Steward server into a file only the
`steward` user can read:
```sh
jarl token create dhcp01 --user steward -q > steward.token # on the Jarl server, or with the jarl CLI anywhere
scp steward.token dhcp01:
ssh dhcp01 'sudo install -m 0600 -o steward -g steward steward.token /etc/steward/jarl.token && rm steward.token'
rm steward.token
```
A token file is better than putting the token in `steward.yaml`: the
configuration file is readable by the `steward` group, and gets copied
around more readily.
You can give the token an expiry (`--expires 365d`). Rotate it in Jarl with
`jarl token rotate ID --grace 1d`, then put the new secret in the file and
restart stewardd within the grace period. Since the certificate is renewed
every couple of months, nothing breaks while you do.
## 3. Steward's settings
In `/etc/steward/steward.yaml`:
```yaml
jarl:
server: https://dns.adbraun.lol:8053
token_file: /etc/steward/jarl.token
# ca_cert: /etc/steward/jarl-ca.pem # if Jarl's certificate isn't from a public CA
```
Then restart stewardd (`systemctl restart steward`). Steward needs to
reach Jarl's API over the network: Jarl's `listen` must include an
address Steward can reach, ideally over HTTPS.
`steward cert check` ([certificates.md](certificates.md#checking)) tests
the connection, the token's permission, and that the record reaches the
internet.