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

+53 -3
View File
@@ -4,6 +4,7 @@ import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -26,7 +27,7 @@ func TestWriteVerifyInspect(t *testing.T) {
src := newDB(t)
dir := filepath.Join(t.TempDir(), "backups")
now := time.Date(2026, 10, 8, 3, 15, 12, 0, time.UTC)
f, err := Write(ctx, src, dir, KindJarl, now)
f, err := Write(ctx, src, dir, KindJarl, now, "")
if err != nil {
t.Fatal(err)
}
@@ -44,7 +45,7 @@ func TestWriteVerifyInspect(t *testing.T) {
t.Errorf("inspect: %+v %v", in, err)
}
// The same second again is refused rather than overwritten.
if _, err := Write(ctx, src, dir, KindJarl, now); err == nil {
if _, err := Write(ctx, src, dir, KindJarl, now, ""); err == nil {
t.Error("overwrote a backup")
}
// No temporary files are left.
@@ -54,7 +55,7 @@ func TestWriteVerifyInspect(t *testing.T) {
}
// A missing database is an error, and isn't created.
missing := filepath.Join(t.TempDir(), "nope.db")
if _, err := Write(ctx, missing, dir, KindJarl, now.Add(time.Second)); err == nil {
if _, err := Write(ctx, missing, dir, KindJarl, now.Add(time.Second), ""); err == nil {
t.Error("backed up a missing database")
}
if _, err := os.Stat(missing); err == nil {
@@ -149,3 +150,52 @@ func TestPruneTempFiles(t *testing.T) {
t.Error("a running backup's temporary file was deleted")
}
}
func TestKeyFiles(t *testing.T) {
ctx := context.Background()
src := newDB(t)
keys := t.TempDir()
os.MkdirAll(filepath.Join(keys, "view-3"), 0o755)
os.WriteFile(filepath.Join(keys, "view-3", "Kexample.com.+013+12345.key"), []byte("public"), 0o644)
os.WriteFile(filepath.Join(keys, "view-3", "Kexample.com.+013+12345.private"), []byte("private-key-material-xyz"), 0o600)
dir := t.TempDir()
f, err := Write(ctx, src, dir, KindJarl, time.Now(), keys)
if err != nil {
t.Fatal(err)
}
root, files, err := Files(ctx, f.Path)
if err != nil || root != keys || len(files) != 2 {
t.Fatalf("files: %q %+v %v", root, files, err)
}
if files[1].Path != "view-3/Kexample.com.+013+12345.private" || string(files[1].Data) != "private-key-material-xyz" || files[1].Mode != 0o600 {
t.Errorf("private key: %+v", files[1])
}
if in, err := Inspect(ctx, f.Path); err != nil || in.KeyFiles != 2 {
t.Errorf("inspect: %+v %v", in, err)
}
// A restored database loses the table again.
restored := filepath.Join(t.TempDir(), "jarl.db")
b, _ := os.ReadFile(f.Path)
os.WriteFile(restored, b, 0o600)
if err := DropFiles(ctx, restored); err != nil {
t.Fatal(err)
}
if _, files, _ := Files(ctx, restored); len(files) != 0 {
t.Error("files left after DropFiles")
}
if b, _ := os.ReadFile(restored); strings.Contains(string(b), "private-key-material-xyz") {
t.Error("the private key is still in the file")
}
// No key directory: nothing stored. An unreadable key: an error.
if f, err := Write(ctx, src, dir, KindJarl, time.Now().Add(time.Second), filepath.Join(keys, "missing")); err != nil {
t.Error(err)
} else if _, files, _ := Files(ctx, f.Path); len(files) != 0 {
t.Errorf("files from a missing directory: %v", files)
}
if os.Geteuid() != 0 {
os.Chmod(filepath.Join(keys, "view-3", "Kexample.com.+013+12345.private"), 0)
if _, err := Write(ctx, src, dir, KindJarl, time.Now().Add(2*time.Second), keys); err == nil || !strings.Contains(err.Error(), "back up as root") {
t.Errorf("unreadable key: %v", err)
}
}
}