Files
steward/docs/certificates.md
T
abrown38andClaude Opus 5.5 7b3f0f764c 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>
2026-10-14 15:46:36 +01:00

5.5 KiB

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).
  • 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.

  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:

    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:

    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):

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.