Add DNSSEC signing, key backups and Porkbun support

Zones get a DNSSEC setting: named signs them with BIND's default (or a
custom) dnssec-policy and inline signing, with keys in a per-view key
directory. Jarl shows the keys, the DS record and when it is safe to
publish it, checks the parent's DS records (check-ds, which also tells
named), and refuses steps that would break a signed domain: switching
straight to unsigned, finishing a turn-off too early, deleting or moving
a signed zone, TTLs above max-zone-ttl. Turning off goes through the
insecure policy; wipe starts over once the parent has no DS.

Backups store the DNSSEC keys and restore puts them back for named; the
backup unit gets CAP_DAC_READ_SEARCH to read them. Jarl raises a zone's
serial above named's whenever it writes an older one (after a restore).
The serials check compares the signed serial.

With a Porkbun API key in jarl.yaml, the registrar's name servers, glue
records and DS records can be shown and changed, each change checked
first (dry run, force to override).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-08 21:19:04 +00:00
1 parent 293e6ddd33
commit 5630e7a848
72 files changed
+5760 -153

No files matched your search

+7
View File
@@ -4,6 +4,8 @@ import { rerender } from "../app.js";
import { optionsEditor } from "./options.js";
import { emailOf, pendingBadge } from "./zones.js";
import { isReverseZone, reverseInfo, ptrOwner, addressOf } from "../names.js";
import { dnssecTab } from "./dnssec.js";
import { registrarTab } from "./registrar.js";
export async function render(el, params, query) {
const id = params.id;
@@ -15,6 +17,8 @@ export async function render(el, params, query) {
["options", "Options"],
zone.type === "primary" ? ["file", "Zone file"] : null,
["primary", "secondary", "mirror"].includes(zone.type) ? ["servers", "Name servers"] : null,
zone.type === "primary" ? ["dnssec", "DNSSEC"] : null,
zone.registrar && state.me && state.me.admin ? ["registrar", "Registrar"] : null,
].filter(Boolean);
const body = h("div");
mount(el,
@@ -26,6 +30,7 @@ export async function render(el, params, query) {
zone.pending ? [pendingBadge(zone.pending), " "] : null,
zone.view ? ["view ", h("a", { href: "#/views" }, zone.view), " · "] : null,
zone.type === "primary" ? "serial " + zone.soa.serial + " · " : null,
zone.dnssec_policy ? [h("a", { href: "#/zones/" + id + "/dnssec" }, badge(zone.dnssec_policy === "insecure" ? "DNSSEC: turning off" : "DNSSEC", zone.dnssec_policy === "insecure" ? "warn" : "ok")), " "] : null,
zone.zone_file ? h("span", { class: "mono small" }, zone.zone_file) : null)),
h("div", { class: "actions" },
h("button", { class: "danger", onclick: () => deleteZone(zone) }, "Delete zone"))),
@@ -36,6 +41,8 @@ export async function render(el, params, query) {
else if (tab === "options") await optionsEditor(body, { scope: "zone", scope_id: zone.id, catalogScope: "zone:" + zone.type, title: "Zone options" });
else if (tab === "file") await fileTab(body, zone);
else if (tab === "servers") await serversTab(body, zone);
else if (tab === "dnssec") await dnssecTab(body, zone);
else if (tab === "registrar") await registrarTab(body, zone);
}
// ---- name servers: serials on the other servers, and the delegation