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

+4 -1
View File
@@ -13,6 +13,7 @@ import * as history from "./pages/history.js";
import * as audit from "./pages/audit.js";
import * as users from "./pages/users.js";
import * as account from "./pages/account.js";
import * as certificate from "./pages/certificate.js";
const routes = [
{ pattern: /^$/, page: dashboard, nav: "dashboard" },
@@ -27,6 +28,7 @@ const routes = [
{ pattern: /^audit$/, page: audit, nav: "audit" },
{ pattern: /^users$/, page: users, nav: "users" },
{ pattern: /^account$/, page: account, nav: "account" },
{ pattern: /^certificate$/, page: certificate, nav: "certificate" },
];
// The top bar. ["key", "#/href", "Label", permission?]
@@ -39,6 +41,7 @@ const navItems = [
["history", "#/history", "History", "config"],
{ menu: "Server", items: [
["settings", "#/settings", "Settings and options", "config"],
["certificate", "#/certificate", "Certificate", "admin"],
["audit", "#/audit", "Audit log", "audit"],
] },
];
@@ -116,7 +119,7 @@ function footer(b) {
}
function renderNav() {
const allowed = (n) => !n[3] || can(n[3], false);
const allowed = (n) => !n[3] || (n[3] === "admin" ? state.me.admin : can(n[3], false));
const link = ([key, href, label]) => h("a", { href, class: current.nav === key ? "active" : "" }, label);
mount(navEl, navItems.map((n) => {
if (!n.menu) return allowed(n) ? link(n) : null;