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

+37 -18
View File
@@ -6,7 +6,8 @@
|------|-----|
| Is everything healthy? | `jarl status`, or the web UI dashboard: named reachable, adoption, drift, open changesets, configuration problems, the newest backup, tokens about to expire |
| Are the secondaries up to date? | `jarl zone serials ZONE`, or the zone's Name servers tab ([below](#checking-name-servers-and-the-delegation)) |
| Back up / restore | Daily by `jarl-backup.timer`; by hand `sudo -u jarl jarld backup`; restore with `jarld restore` ([below](#backups)) |
| Back up / restore | Daily by `jarl-backup.timer`; by hand `sudo jarld backup`; restore with `jarld restore` ([below](#backups)) |
| DNSSEC | `jarl zone dnssec status ZONE`, or the zone's DNSSEC tab ([dnssec.md](dnssec.md)) |
| What changed and who did it? | `jarl history`, `jarl changeset show ID` (with diff), `jarl audit` |
| Undo a change | `jarl changeset revert ID`, review it, then `jarl changeset apply NEW_ID` (or History → Revert in the UI) |
| Check the configuration | `jarl check`: full validation plus `named-checkconf -z` |
@@ -23,8 +24,10 @@ loaded. jarld is only needed to make changes.
Everything Jarl knows is in its database (`/var/lib/jarl/jarl.db`): zones,
records, views, ACLs, TSIG keys, options, users, tokens, history and the
audit log. named's files are generated from it, so the database is the one
thing to back up.
audit log. named's files are generated from it. The one exception is the
DNSSEC keys of signed zones, which named keeps in Jarl's key directory
(`/var/lib/bind/jarl/dnssec-keys`): every backup stores those too, inside the
backup file ([dnssec.md](dnssec.md#keys-backups-and-moving-servers)).
### The daily backup
@@ -41,17 +44,19 @@ Each run:
1. writes a consistent copy of the database with SQLite's `VACUUM INTO`
(safe while jarld runs and changes things) to a temporary file in the
backup directory;
2. checks the copy with SQLite's `integrity_check`, and only then gives it
2. stores the DNSSEC keys in it (every file of the key directory, private
keys included);
3. checks the copy with SQLite's `integrity_check`, and only then gives it
its name, `jarl-<UTC time>.db`, e.g. `jarl-20261008T031512Z.db`. A file
with that name is always a complete, intact backup;
3. deletes backups older than `retention`, but always keeps the newest
4. deletes backups older than `retention`, but always keeps the newest
`keep_at_least`, however old they are. If backups stop for a while (the
machine was off, the disk was full), the old ones aren't all deleted by
the next run. Ages come from the time in the name, not the file's
modification time, which copying the directory would change. Only files
named `jarl-<time>.db` (and `stats-<time>.db`) are ever deleted, so
anything else you put in the directory is safe;
4. records the result in the database, so the dashboard and `jarl status`
5. records the result in the database, so the dashboard and `jarl status`
show when the newest backup was made, and whether the last attempt
failed.
@@ -87,7 +92,9 @@ systemctl edit jarl-backup.timer
The backup service runs as the `jarl` user in a sandbox that may only
write to `/var/lib/jarl` and `/var/backups/jarl` (created by
`tmpfiles.d/jarl.conf`, mode 0700). To keep backups elsewhere, set
`tmpfiles.d/jarl.conf`, mode 0700). It has one capability,
`CAP_DAC_READ_SEARCH`, to read named's DNSSEC private keys, which only
named's user may read. To keep backups elsewhere, set
`backup.dir`, create the directory (owned by `jarl`, mode 0700) and allow
it:
@@ -104,18 +111,23 @@ well.
### By hand
```sh
sudo -u jarl jarld backup # into backup.dir, as the timer does
sudo -u jarl jarld backup /root/before-upgrade.db # one copy, never deleted automatically
sudo jarld restore # list the backups in backup.dir
sudo jarld backup # into backup.dir, as the timer does
sudo jarld backup /root/before-upgrade.db # one copy, never deleted automatically
sudo jarld restore # list the backups in backup.dir
sudo systemctl start jarl-backup # or exactly what the timer runs
```
Query statistics live in a separate database (`/var/lib/jarl/stats.db`).
DNS doesn't need it, so it is backed up only with `backup.stats: true`, as
`stats-<UTC time>.db`.
Run it as root (files it creates are given to `jarl`): named's DNSSEC
private keys are readable only by named's user. As another user it fails
when there are keys; `--without-keys` backs up only the database.
The database holds password and token *hashes*, and TSIG secrets in the
clear. Backups are written with mode 0600 into a 0700 directory; keep any
copies just as private.
clear, and backups hold the DNSSEC private keys too. Backups are written
with mode 0600 into a 0700 directory; keep any copies just as private.
### Restoring
@@ -138,7 +150,11 @@ changing anything (`--yes` skips the question). Then it:
`jarl.db.before-restore-<UTC time>` (with its `-wal` and `-shm` files),
so a restore can be undone the same way;
* copies the backup in place, upgrades it to the current schema if it is
older, and records the restore in its audit log.
older, and records the restore in its audit log;
* puts the DNSSEC keys stored in the backup back into the key directory,
owned by named's user (`bind.named_user`) with the modes they had. Key
files that are there but not in the backup are left alone.
`--without-keys` skips this; without it, a backup with keys needs root.
Then start jarld and bring named in line with the restored database:
@@ -152,11 +168,14 @@ jarl resync --force # write everything and reload named
Things to know:
* **Zone serials.** An older backup can carry lower zone serials than named
last served. Your own named doesn't mind, but secondaries and caches
ignore a serial that went backwards, so they keep the newer data. If you
have secondaries, raise the serials after a restore:
`jarl zone update ZONE --serial <higher than before>`, then check with
`jarl zone serials ZONE`.
last served. Secondaries ignore a serial that went backwards, and named
refuses one for a signed zone. Jarl handles it: whenever it writes a zone
file whose serial isn't above the one named has loaded, it raises the
serial past named's (the resync result and the audit log show it). Check
the secondaries afterwards with `jarl zone serials ZONE`.
* **DNSSEC keys** must be back before named loads a signed zone, or named
makes new keys that the parent's DS record doesn't match. On a new
machine, restore before adopting.
* **Tokens and passwords** go back to what they were when the backup was
made: tokens created since are gone, and revoked ones work again. Rotate
or revoke as needed.