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>
121 lines
5.5 KiB
Markdown
121 lines
5.5 KiB
Markdown
# The web UI's certificate
|
|
|
|
Steward can serve its web UI and API over HTTPS in two ways:
|
|
|
|
* **A certificate you manage**: `tls.cert_file` and `tls.key_file` in
|
|
`steward.yaml`. stewardd rereads them when they change
|
|
([configuration.md](configuration.md#https)).
|
|
* **A certificate it gets itself** from Let's Encrypt (or another ACME CA),
|
|
with Jarl publishing the DNS-01 challenge. That's this page.
|
|
|
|
With DNS-01 the web UI doesn't need to be reachable from the internet.
|
|
`dhcp.adbraun.lol` can point at a private address and the certificate
|
|
still works on your network. Only the challenge record has to be visible
|
|
on the internet, and Jarl puts it in every view that has your zone.
|
|
|
|
## Setting it up
|
|
|
|
1. **Connect Steward to Jarl** with a user and a token that may only write
|
|
Steward's own challenge record: [jarl.md](jarl.md).
|
|
2. **Give your web UI a name in DNS**, such as `dhcp.adbraun.lol` with the
|
|
Steward server's address, in Jarl. It is the name you will type in the
|
|
browser.
|
|
3. **Configure the certificate** in `/etc/steward/steward.yaml`:
|
|
|
|
```yaml
|
|
listen: ["10.0.10.2:8067"] # an address your browser can reach
|
|
tls:
|
|
acme:
|
|
domain: dhcp.adbraun.lol
|
|
email: you@adbraun.lol # Let's Encrypt writes here if something is wrong
|
|
directory: letsencrypt-staging # first; see below
|
|
agree_tos: true # you accept Let's Encrypt's terms of service
|
|
jarl:
|
|
server: https://dns.adbraun.lol:8053
|
|
token_file: /etc/steward/jarl.token
|
|
```
|
|
|
|
4. **Restart stewardd** (`sudo systemctl restart steward`), and check:
|
|
|
|
```sh
|
|
sudo steward cert check
|
|
```
|
|
|
|
5. **Watch it get the certificate:** `sudo steward cert`, or Server →
|
|
Certificate in the web UI. Then open `https://dhcp.adbraun.lol:8067/`.
|
|
6. **Switch to the real CA.** The staging certificate works, but browsers
|
|
warn about it, because Let's Encrypt's staging service isn't trusted.
|
|
It's there to prove everything works without touching Let's Encrypt's
|
|
rate limits. Once it does, set `directory: letsencrypt`, restart, and
|
|
Steward gets a trusted certificate right away.
|
|
|
|
Until the first certificate arrives, the web UI is served over plain
|
|
HTTP. stewardd switches to HTTPS on the same port without a restart. From
|
|
then on, plain HTTP is redirected to HTTPS, except from the server itself
|
|
(`127.0.0.1`), so tools there keep working.
|
|
|
|
## Checking
|
|
|
|
Before every certificate order, and whenever you ask (`steward cert
|
|
check`, or Test in the web UI), Steward checks the whole path without
|
|
asking the CA. Let's Encrypt allows only 5 failed validations per name
|
|
per hour, so a broken setup shouldn't find out the hard way.
|
|
|
|
| Step | What it checks |
|
|
|---|---|
|
|
| Jarl | Jarl answers at `jarl.server`, and the token is valid (it says whose it is) |
|
|
| Zone | a primary zone the token can see holds `_acme-challenge.<domain>`, and in which views |
|
|
| Permission | the token may write the TXT record there (Jarl checks it as a dry run) |
|
|
| Write | a test record with a random value is written through Jarl, which loads it into named |
|
|
| Name servers | each of the zone's public name servers serves it, asked directly as the CA's resolvers will. A name server passes if any of its addresses does (`tls.acme.check_propagation`) |
|
|
| Public resolvers | the resolvers in `tls.acme.resolvers` (1.1.1.1, 9.9.9.9, 8.8.8.8) see it. If the name servers do but a resolver doesn't, it is only a warning: it may be holding an earlier answer |
|
|
| Clean up | the test record is removed |
|
|
|
|
Each step says what went wrong and what to do. For example:
|
|
|
|
```
|
|
ok Jarl the token is steward's, on Jarl v0.6.1
|
|
ok Zone adbraun.lol in views internal, external
|
|
FAIL Permission the token may not write the challenge record (...); give it the grant:
|
|
records:write zones=adbraun.lol names=_acme-challenge.dhcp.adbraun.lol types=TXT
|
|
```
|
|
|
|
The checks wait up to 90 seconds for the record to appear. A failing step
|
|
lists the DNS questions it asked, as `dig` commands, so you can ask them
|
|
yourself.
|
|
|
|
If the challenge name is a CNAME into another zone, the check follows it,
|
|
as Let's Encrypt does.
|
|
|
|
## Renewal
|
|
|
|
A certificate is renewed when a third of its lifetime is left: 30 days
|
|
before the end for Let's Encrypt's 90-day certificates, 2 days for 6-day
|
|
ones. stewardd looks twice a day. The check runs first every time, and
|
|
the new certificate is used at once, without a restart.
|
|
|
|
If renewing fails, the old certificate keeps being served, and stewardd
|
|
tries again after 1 hour, then 3, 6 and every 12. The dashboard shows a
|
|
problem (`certificate_failed`), and `certificate_expiring` once less than
|
|
14 days are left. `steward cert renew` (Renew now in the web UI) tries
|
|
again at once.
|
|
|
|
The ACME account and the certificate, with its key, are kept in Steward's
|
|
database, so backups include them.
|
|
|
|
## Settings
|
|
|
|
All under `tls.acme` in `steward.yaml` ([configuration.md](configuration.md)):
|
|
|
|
| Setting | Default | |
|
|
|---|---|---|
|
|
| `domain` | none (off) | the name on the certificate |
|
|
| `email` | none | the account's contact address |
|
|
| `directory` | `letsencrypt-staging` | `letsencrypt`, `letsencrypt-staging`, or an ACME directory URL (a private CA such as step-ca) |
|
|
| `agree_tos` | `false` | accept the CA's terms of service; no account is made without it |
|
|
| `resolvers` | `1.1.1.1:53`, `9.9.9.9:53`, `8.8.8.8:53` | public resolvers, to see the challenge as the CA does |
|
|
| `check_propagation` | `true` | also ask each of the zone's name servers directly |
|
|
| `ca_bundle` | none | extra CAs to trust for the ACME server itself |
|
|
|
|
`tls.cert_file` and `tls.acme` can't both be set.
|