Certificates for other hosts: jarl cert get
jarld obtains and renews a certificate for another host; the host asks
for it with one command, waits, downloads it when it is newer than its
copy and runs a hook with the paths in JARL_CERT_* variables.
- Certificates have an owner user; another user can't take the name.
An existing one is obtained again only when a name is added or
removed. A new certificates grant (names=) says which names a user
may have; it is never part of "*". Users may own
certificates.max_per_user certificates (10) or their own cert_limit.
- POST /certificates/{name}/request; owners may list, show, export and
delete theirs; keys only over HTTPS, the socket or loopback; every
download is recorded and shown as "Pulled by".
- CLI: jarl cert get (saved settings, all saved when no name), cert
saved, cert forget, cert schedule (daily systemd timer, Linux, root).
- Migration 14. Tests: server, auth, and e2e against Pebble.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b4bb99b482
commit
9db76a113c
29 files changed
+1568
-40
No files matched your search
@@ -35,6 +35,7 @@ export async function render(el) {
|
||||
c.uses.includes("web") ? [badge(c.serving ? "web UI (serving)" : "web UI", c.serving ? "ok" : ""), " "] : null,
|
||||
c.uses.includes("dns") ? namedUse(c) : null,
|
||||
!c.uses.length ? h("span", { class: "muted", title: "Export it to use it in another program" }, "not used by Jarl") : null] },
|
||||
{ label: "Owner", render: (c) => c.owner ? h("span", null, c.owner, pulledBy(c)) : h("span", { class: "muted" }, "Jarl") },
|
||||
{ label: "Source", render: (c) => c.source === "acme" ? h("span", null, "Jarl", h("div", { class: "small muted" }, "challenge " + c.challenge)) : h("span", null, "your files", h("div", { class: "small muted mono" }, c.cert_file)) },
|
||||
{ label: "Expires", render: (c) => c.not_after ? h("span", null, time(c.not_after), c.source === "acme" && c.next_attempt ? h("div", { class: "small muted" }, "renews " + fmtTime(c.next_attempt)) : null) : h("span", { class: "muted" }, "–") },
|
||||
{ label: "Status", render: (c) => {
|
||||
@@ -64,6 +65,15 @@ export async function render(el) {
|
||||
}
|
||||
}
|
||||
|
||||
// pulledBy says who downloaded a certificate for another host, and
|
||||
// whether they have the current one.
|
||||
function pulledBy(c) {
|
||||
const ps = c.pulled_by || [];
|
||||
if (!ps.length) return h("div", { class: "small muted" }, "not downloaded yet");
|
||||
return ps.map((p) => h("div", { class: "small " + (p.serial === c.serial ? "muted" : "warn-text"), title: p.who + " from " + (p.ip || "?") + ", " + fmtTime(p.at) },
|
||||
p.who.replace(/^token /, "") + ", " + fmtTime(p.at) + (p.serial === c.serial ? "" : " (old: not fetched since the renewal)")));
|
||||
}
|
||||
|
||||
// namedUse is what named uses a certificate for: DoT and DoH, with the
|
||||
// addresses in the tooltip (the details list them).
|
||||
function namedUse(c) {
|
||||
|
||||
@@ -54,6 +54,7 @@ async function userDialog(u, grants) {
|
||||
tmpl.value = "";
|
||||
});
|
||||
const comment = input({ value: u ? u.comment : "" });
|
||||
const certLimit = input({ type: "number", min: "0", value: u && u.cert_limit != null ? String(u.cert_limit) : "", placeholder: "certificates.max_per_user" });
|
||||
await modal(u ? "Edit " + u.username : "New user", h("div", null, h("div", { class: "form-grid" },
|
||||
field("Username", name),
|
||||
field("Password", pw),
|
||||
@@ -62,6 +63,7 @@ async function userDialog(u, grants) {
|
||||
u ? h("span") : null, u ? disabled : null,
|
||||
field("Permissions", gr, "One grant per line (ignored for administrators)."),
|
||||
h("span"), tmpl,
|
||||
field("Certificates", certLimit, "How many certificates this user may own through jarl cert get (needs a certificates grant). Empty: the default from jarl.yaml."),
|
||||
field("Comment", comment)), err), [
|
||||
{ label: "Cancel", value: null },
|
||||
{ label: u ? "Save" : "Create user", kind: "primary", onclick: async () => {
|
||||
@@ -69,6 +71,8 @@ async function userDialog(u, grants) {
|
||||
if (service.input.checked) data.password = "";
|
||||
else if (pw.value) data.password = pw.value;
|
||||
if (u) data.disabled = disabled.input.checked;
|
||||
if (certLimit.value.trim() !== "") data.cert_limit = Number(certLimit.value);
|
||||
else if (u && u.cert_limit != null) data.cert_limit = -1;
|
||||
try {
|
||||
if (u) await api("PATCH", "/users/" + u.id, { body: data });
|
||||
else await api("POST", "/users", { body: data });
|
||||
|
||||
Reference in new issue
Block a user