Add backups, token expiry warnings and name server checks
- Daily database backups: jarl-backup.timer runs 'jarld backup', which writes jarl-<UTC time>.db into backup.dir, checks it before naming it, and prunes by the time in the name while always keeping the newest keep_at_least. New backup: section in jarl.yaml; the dashboard and 'jarl status' show the newest backup and warn when it is stale or failed. The Debian package enables the timer. - 'jarld restore [FILE | latest]': checks the backup, shows it next to the current database, keeps the current one aside and records the restore. 'jarld serve' holds a database lock, so a restore can't run under a live jarld and two jarlds can't share a database. - Token expiry warnings from 14 days ahead: dashboard, token lists, a daily log line, and a CLI warning via the Jarl-Token-Expires header; 'jarl token list --expiring'. - 'jarl zone serials' and the zone page's Name servers tab: compare the serial on a primary zone's secondaries (NS records, also-notify) or a secondary zone's primaries with this server's. - 'jarl zone delegation': for zones on the internet, check the parent's delegation and glue against the zone; an undelegated zone gets a note. - Server: Jarl/<version> header; CLI exit code 2 for failed checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1afed741bf
commit
293e6ddd33
47 files changed
+3717
-95
No files matched your search
+37
-1
@@ -39,6 +39,10 @@ sudo curl --unix-socket /run/jarl/jarl.sock http://localhost/api/v1/status
|
||||
**HTTP Basic**, only on the lego `httpreq` routes (below): the username is
|
||||
the token's user and the password the API token.
|
||||
|
||||
A response to a token that expires within 14 days carries
|
||||
`Jarl-Token-Expires: <RFC 3339 time>`; the `jarl` CLI warns when it sees
|
||||
it. Every response has `Server: Jarl/<version>`.
|
||||
|
||||
## Errors
|
||||
|
||||
```http
|
||||
@@ -138,6 +142,8 @@ may be the name.
|
||||
| PATCH | `/zones/{zone}` | ZoneInput (only the fields to change) |
|
||||
| DELETE | `/zones/{zone}` | deletes the zone with its records and options |
|
||||
| GET | `/zones/{zone}/file` | the generated zone file (text/plain) |
|
||||
| GET | `/zones/{zone}/serials` | asks the zone's other name servers for their serial (below) |
|
||||
| GET | `/zones/{zone}/delegation` | checks the parent zone's delegation of a primary zone (below); `?resolver=IP` |
|
||||
| POST | `/zones/{zone}/import` | `{"text": "<zone file>", "mode": "merge"\|"replace", "use_soa": false, "dry_run": false}` |
|
||||
|
||||
ZoneInput:
|
||||
@@ -162,6 +168,36 @@ ZoneInput:
|
||||
}
|
||||
```
|
||||
|
||||
`/serials` and `/delegation` send DNS queries from the server, and only
|
||||
when called ([operations.md](operations.md#checking-name-servers-and-the-delegation)).
|
||||
`/serials` works for primary, secondary and mirror zones:
|
||||
|
||||
```json
|
||||
{
|
||||
"zone": "adbraun.lol.", "view": "", "type": "primary",
|
||||
"serial": 2026100814, "serial_source": "named", // or "jarl" when named can't be asked
|
||||
"servers": [
|
||||
{"name": "ns1.adbraun.lol.", "address": "192.168.1.53", "source": "NS record", "status": "self", "serial": 2026100814},
|
||||
{"name": "ns2.adbraun.lol.", "address": "192.168.1.54", "source": "NS record", "status": "behind",
|
||||
"serial": 2026100812, "diff": -2, "rtt_ms": 1}
|
||||
],
|
||||
"notes": [], "checked_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
`status` is `self`, `in_sync`, `answered` (no serial here to compare),
|
||||
`behind`, `ahead`, `not_authoritative`, `refused`, `unreachable`,
|
||||
`no_route` (no route from this server, e.g. IPv6), `no_address` or
|
||||
`error` (with `error`). `source` is `NS record`, `also-notify` or
|
||||
`primaries`; `port` is set for a non-default port.
|
||||
|
||||
`/delegation` returns `parent`, `resolver`, `delegated`, `parent_ns`,
|
||||
`jarl_ns`, `parent_servers` (each parent server's `status`: `delegated`,
|
||||
`not_delegated`, `unreachable`, `error`, with `ns` and `glue`), `glue`
|
||||
(`name`, `parent`, `jarl`, `match`), `servers` (as above, for the delegated
|
||||
name servers) and `findings`: `{"level": "ok"|"info"|"warning"|"error",
|
||||
"message": "..."}`. A zone that isn't delegated gets one `info` finding.
|
||||
|
||||
### Records
|
||||
|
||||
| Method | Path | Body / query |
|
||||
@@ -274,7 +310,7 @@ in it and its records.
|
||||
|
||||
| Method | Path | Notes |
|
||||
|--------|------|-------|
|
||||
| GET | `/status` | named status, adoption, drift, open changesets, current problems, object counts |
|
||||
| GET | `/status` | named status, adoption, drift, open changesets, current problems, object counts; `expiring_tokens` (tokens that expire within 14 days or expired in the last 14: all for administrators, otherwise the caller's); `backup` (administrators: `last_at`, `last_file`, `last_size`, `last_error`, `last_error_at`, `dir`, `warn_after` in seconds, `stale`) |
|
||||
| POST | `/system/check` | validation + named-checkconf of the current configuration (`options:read`) |
|
||||
| POST | `/system/resync` | `{"dry_run": false, "force": false}`: rewrite every file from the database and reload (admin) |
|
||||
| GET | `/audit` | `?user=&action=&outcome=&object_type=&changeset=&q=&since=&until=&limit=200&before=ID` |
|
||||
|
||||
+18
-1
@@ -135,6 +135,7 @@ to stderr with `-o json` or `-o yaml`, so stdout holds just the documents.
|
||||
|------|---------|
|
||||
| 0 | Success |
|
||||
| 1 | Other error (bad arguments, connection failure, ...) |
|
||||
| 2 | A check found problems (`zone serials`, `zone delegation`); its report was printed |
|
||||
| 3 | Not authenticated, or not allowed (token lacks the permission) |
|
||||
| 4 | Not found |
|
||||
| 5 | Conflict: concurrent change, object in use, drift |
|
||||
@@ -167,6 +168,8 @@ jarl zone update adbraun.lol --move-to-view internal
|
||||
jarl zone delete lab.adbraun.lol [-y]
|
||||
jarl zone file adbraun.lol # the generated zone file
|
||||
jarl zone import adbraun.lol db.adbraun.lol [--replace] [--use-soa]
|
||||
jarl zone serials adbraun.lol # are the secondaries (or primaries) on the current serial?
|
||||
jarl zone delegation adbraun.lol [--resolver 9.9.9.9] # does the parent zone delegate it correctly?
|
||||
```
|
||||
|
||||
Reverse names follow the dots (the nibbles in IPv6), so a reverse zone
|
||||
@@ -185,6 +188,14 @@ are deleted. Try it with `--dry-run` first. `--option "NAME VALUE"` on
|
||||
When a zone exists in several views, add `--view NAME` to commands that
|
||||
name it.
|
||||
|
||||
`zone serials` asks the zone's other name servers which serial they serve
|
||||
(the NS records and also-notify targets of a primary zone, the primaries
|
||||
of a secondary) and exits with 2 if one isn't on the current serial.
|
||||
`zone delegation` is for zones on the internet: it checks the parent
|
||||
zone's delegation and glue against the zone, and calls a zone that isn't
|
||||
delegated (an internal-only zone) a note, not a problem. Both only run when
|
||||
you run them; see [operations.md](operations.md#checking-name-servers-and-the-delegation).
|
||||
|
||||
## Records
|
||||
|
||||
```sh
|
||||
@@ -312,6 +323,7 @@ jarl user update bob --disabled # logs bob out, blocks hi
|
||||
jarl user delete bob -y
|
||||
|
||||
jarl token list [--user alice | --all] [--revoked]
|
||||
jarl token list --all --expiring [60d] # expiring within 14 days (or 60d), or expired
|
||||
jarl token create laptop --expires 365d
|
||||
jarl token create backup-script --grant "*:read"
|
||||
jarl token create acme --user certbot --role acme
|
||||
@@ -321,13 +333,18 @@ jarl token revoke 7
|
||||
|
||||
jarl audit [--user alice] [--action auth.login_failed] [--outcome failure] [--since 2026-10-01] [--limit 100]
|
||||
jarl search 192.168.1.20
|
||||
jarl status # named, adoption, drift, open changesets, problems
|
||||
jarl status # named, adoption, drift, open changesets, backups, expiring tokens, problems
|
||||
jarl check # validation + named-checkconf of the current configuration
|
||||
jarl resync [--force] [--dry-run] # rewrite every file from the database
|
||||
```
|
||||
|
||||
Grant syntax, roles and token rotation are in [permissions.md](permissions.md).
|
||||
|
||||
When the token `jarl` uses expires within 14 days, every command prints a
|
||||
warning on stderr (once), naming the expiry and how to rotate it. Scripts
|
||||
such as certbot hooks show it in their logs well before the token stops
|
||||
working.
|
||||
|
||||
## Query statistics
|
||||
|
||||
```sh
|
||||
|
||||
@@ -36,6 +36,25 @@ Durations accept Go syntax (`90s`, `15m`, `12h`) and days (`30d`).
|
||||
| `staging_dir` | `/var/lib/jarl/staging` | Scratch space where candidate configurations are rendered and checked with named-checkconf before they are installed. |
|
||||
| `audit.retention_days` | `0` | Delete audit events older than this many days (0 keeps them forever). Changeset history is kept regardless. |
|
||||
|
||||
### Backups
|
||||
|
||||
`jarld backup` without a file name, which the `jarl-backup.timer` systemd
|
||||
timer runs daily, writes `jarl-<UTC time>.db` into `backup.dir` and then
|
||||
deletes old backups there. It reads these settings from this file each
|
||||
time it runs; when it runs is the timer's (`systemctl edit
|
||||
jarl-backup.timer`). See [operations.md](operations.md#backups), also for
|
||||
restoring.
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|-----|---------|---------|
|
||||
| `backup.dir` | `/var/backups/jarl` | Where backups go. The backup service's sandbox may only write here; to use another directory, allow it with `ReadWritePaths=` in `systemctl edit jarl-backup.service`. |
|
||||
| `backup.retention` | `30d` | Delete backups older than this, by the time in their name. At least `1d`; `0` keeps every backup. |
|
||||
| `backup.keep_at_least` | `7` | Always keep this many of the newest backups, however old, so a long gap in backups never deletes them all. At least 1. |
|
||||
| `backup.stats` | `false` | Also back up the query statistics database (`stats-<UTC time>.db`). |
|
||||
| `backup.warn_after` | `2d` | The dashboard and `jarl status` warn when the newest backup is older than this. Keep it above the timer's interval; `0` turns the warning off. |
|
||||
|
||||
Only files named `jarl-<time>.db` and `stats-<time>.db` are ever deleted.
|
||||
|
||||
### Logging
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|
||||
+4
-2
@@ -3,7 +3,7 @@
|
||||
## Layout
|
||||
|
||||
```
|
||||
cmd/jarld/ daemon: serve, init, import, adopt, check, render, resync, backup, user
|
||||
cmd/jarld/ daemon: serve, init, import, adopt, check, render, resync, backup, restore, user
|
||||
cmd/jarl/ CLI
|
||||
internal/
|
||||
model/ configuration objects (State: views, ACLs, keys, zones, records, options) and changesets
|
||||
@@ -24,8 +24,10 @@ internal/
|
||||
web/static/ the web UI: plain ES modules, no build step, embedded with go:embed
|
||||
api/, client/ JSON types and the Go client shared by server and CLI
|
||||
acme/ ACME DNS-01 challenge records, shared by jarl acme and the httpreq endpoint
|
||||
backup/ database backups: write (VACUUM INTO) and check, list, prune by the time in the name
|
||||
nscheck/ DNS queries to other name servers: zone serials and the parent's delegation
|
||||
config/ jarld's YAML configuration
|
||||
deploy/ systemd unit, sysusers.d and tmpfiles.d files
|
||||
deploy/ systemd units (jarld, the daily backup service and timer), sysusers.d and tmpfiles.d files
|
||||
deploy/debian/ the Debian package's control template and postinst/prerm/postrm
|
||||
contrib/acme.sh/ acme.sh DNS API hook
|
||||
contrib/lego/ program for lego's exec DNS provider
|
||||
|
||||
+23
-13
@@ -43,22 +43,28 @@ for any architecture Go supports. apt also installs `bind9` (recommended)
|
||||
if it's missing. Installing the package:
|
||||
|
||||
* puts the binaries in `/usr/bin`, bash completion in
|
||||
`/usr/share/bash-completion/completions`, the systemd unit in
|
||||
`/usr/lib/systemd/system/jarl.service`, the sysusers and tmpfiles files
|
||||
`/usr/share/bash-completion/completions`, the systemd units in
|
||||
`/usr/lib/systemd/system` (`jarl.service`, and `jarl-backup.service`
|
||||
with its daily `jarl-backup.timer`), the sysusers and tmpfiles files
|
||||
in `/usr/lib`, and the docs and contrib files in `/usr/share/doc/jarl`;
|
||||
* creates the `jarl` user (in group `bind`) and Jarl's directories, and
|
||||
writes `/etc/jarl/jarl.yaml` from the example on the first install. No
|
||||
file in `/etc` belongs to the package, so your configuration is never
|
||||
replaced or asked about on upgrades;
|
||||
* turns on the daily database backup, `jarl-backup.timer`
|
||||
([operations.md](operations.md#backups)). It does nothing until the
|
||||
database exists. If you turn it off (`systemctl disable
|
||||
jarl-backup.timer`), upgrades leave it off;
|
||||
* doesn't start jarld: continue with [3. Configure](#3-configure), then
|
||||
`systemctl enable --now jarl` in step 8.
|
||||
|
||||
**Upgrading**: build and install the newer package the same way. A running
|
||||
jarld is restarted, and upgrades its database as it starts.
|
||||
|
||||
**Removing** (`sudo apt remove jarl`) stops jarld and removes the program,
|
||||
but named keeps running with the configuration Jarl last wrote. Even
|
||||
`apt purge jarl` keeps `/etc/jarl`, `/var/lib/jarl` (the database) and the
|
||||
**Removing** (`sudo apt remove jarl`) stops jarld and the backup timer and
|
||||
removes the program, but named keeps running with the configuration Jarl
|
||||
last wrote. Even `apt purge jarl` keeps `/etc/jarl`, `/var/lib/jarl` (the
|
||||
database), `/var/backups/jarl` and the
|
||||
files in `/etc/bind/jarl`, `/var/lib/bind/jarl` and `/var/cache/bind/jarl`,
|
||||
since named.conf may still include them. Remove them by hand once named no
|
||||
longer uses them.
|
||||
@@ -78,8 +84,9 @@ This installs:
|
||||
| `/usr/local/bin/jarld`, `/usr/local/bin/jarl` | daemon and CLI |
|
||||
| `/usr/local/share/bash-completion/completions/jarl`, `.../jarld` | bash completion (loaded automatically by the `bash-completion` package) |
|
||||
| `/etc/systemd/system/jarl.service` | systemd unit (`Type=notify`, sandboxed) |
|
||||
| `/etc/systemd/system/jarl-backup.service`, `jarl-backup.timer` | the daily database backup ([operations.md](operations.md#backups)) |
|
||||
| `/usr/lib/sysusers.d/jarl.conf` | creates the `jarl` system user and adds it to group `bind` |
|
||||
| `/usr/lib/tmpfiles.d/jarl.conf` | creates `/etc/jarl`, `/var/lib/jarl`, `/etc/bind/jarl`, `/var/lib/bind/jarl`, `/var/cache/bind/jarl` |
|
||||
| `/usr/lib/tmpfiles.d/jarl.conf` | creates `/etc/jarl`, `/var/lib/jarl`, `/etc/bind/jarl`, `/var/lib/bind/jarl`, `/var/cache/bind/jarl`, `/var/backups/jarl` |
|
||||
| `/etc/jarl/jarl.yaml` | example configuration (only if none exists) |
|
||||
| `/usr/local/share/doc/jarl/` | these docs and the acme.sh hook |
|
||||
|
||||
@@ -90,19 +97,21 @@ systemd-sysusers jarl.conf
|
||||
systemd-tmpfiles --create jarl.conf
|
||||
chgrp jarl /etc/jarl/jarl.yaml
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now jarl-backup.timer # daily backups; idle until the database exists
|
||||
```
|
||||
|
||||
Without `make` (from a release tarball), the same steps are:
|
||||
|
||||
```sh
|
||||
install -m 0755 jarld jarl /usr/local/bin/
|
||||
install -m 0644 deploy/systemd/jarl.service /etc/systemd/system/
|
||||
install -m 0644 deploy/systemd/jarl.service deploy/systemd/jarl-backup.service deploy/systemd/jarl-backup.timer /etc/systemd/system/
|
||||
install -m 0644 deploy/sysusers.d/jarl.conf /usr/lib/sysusers.d/
|
||||
install -m 0644 deploy/tmpfiles.d/jarl.conf /usr/lib/tmpfiles.d/
|
||||
systemd-sysusers jarl.conf && systemd-tmpfiles --create jarl.conf
|
||||
jarld config --example > /etc/jarl/jarl.yaml
|
||||
chgrp jarl /etc/jarl/jarl.yaml && chmod 0640 /etc/jarl/jarl.yaml
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now jarl-backup.timer
|
||||
```
|
||||
|
||||
### Why the jarl user is in group bind
|
||||
@@ -292,11 +301,12 @@ make && sudo make install
|
||||
sudo systemctl restart jarl
|
||||
```
|
||||
|
||||
Database migrations run automatically at startup. Before an upgrade, back
|
||||
up the database (it is safe while jarld runs):
|
||||
Database migrations run automatically at startup. Before an upgrade, take
|
||||
a backup (it is safe while jarld runs); `jarld restore` puts it back if
|
||||
needed:
|
||||
|
||||
```sh
|
||||
sudo -u jarl jarld backup /var/lib/jarl/jarl-$(date +%F).db
|
||||
sudo -u jarl jarld backup
|
||||
```
|
||||
|
||||
## Uninstalling
|
||||
@@ -304,10 +314,10 @@ sudo -u jarl jarld backup /var/lib/jarl/jarl-$(date +%F).db
|
||||
```sh
|
||||
cp /etc/bind/named.conf.pre-jarl-<date> /etc/bind/named.conf # or write a named.conf that includes Jarl's files
|
||||
rndc reconfig
|
||||
systemctl disable --now jarl
|
||||
systemctl disable --now jarl jarl-backup.timer
|
||||
make uninstall
|
||||
```
|
||||
|
||||
`/etc/bind/jarl` and `/var/lib/bind/jarl` contain plain named.conf and zone
|
||||
files that keep working without Jarl. `make uninstall` leaves them and
|
||||
Jarl's database in place.
|
||||
files that keep working without Jarl. `make uninstall` leaves them,
|
||||
Jarl's database and its backups in place.
|
||||
+223
-26
@@ -4,14 +4,16 @@
|
||||
|
||||
| Task | How |
|
||||
|------|-----|
|
||||
| Is everything healthy? | `jarl status`, or the web UI dashboard: named reachable, adoption, drift, open changesets, configuration problems |
|
||||
| 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)) |
|
||||
| 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` |
|
||||
| Logs | `journalctl -u jarl` (jarld, including `msg=audit` lines), `journalctl -u named` |
|
||||
| Restart / reload TLS | `systemctl restart jarl` / `systemctl reload jarl` |
|
||||
| Work on the server itself | `sudo jarl ...`: root uses jarld's local socket and needs no token ([cli.md](cli.md#on-the-server-itself)) |
|
||||
| Renew or replace a token | `jarl token rotate NAME --grace 1h` (same grants, new secret; [permissions.md](permissions.md#rotating-a-token)) |
|
||||
| Renew or replace a token | `jarl token rotate NAME --grace 1h` (same grants, new secret; [permissions.md](permissions.md#rotating-a-token)). `jarl token list --all --expiring` shows those that expire within 14 days |
|
||||
| Upgrade Jarl | Debian package: build the new one and `sudo apt install ./dist/jarl_*.deb`; otherwise `make && sudo make install && sudo systemctl restart jarl`. jarld upgrades its database when it starts |
|
||||
|
||||
Restarting jarld doesn't affect DNS: named keeps serving the files it
|
||||
@@ -19,46 +21,151 @@ loaded. jarld is only needed to make changes.
|
||||
|
||||
## Backups
|
||||
|
||||
Everything Jarl knows is in its database. The BIND files can be generated
|
||||
from it again at any time.
|
||||
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.
|
||||
|
||||
### The daily backup
|
||||
|
||||
The `jarl-backup.timer` systemd timer runs `jarld backup` once a day. The
|
||||
Debian package turns it on; after `make install`, turn it on yourself:
|
||||
|
||||
```sh
|
||||
sudo -u jarl jarld backup /var/backups/jarl/jarl-$(date +%F).db # consistent copy, safe while jarld runs
|
||||
systemctl enable --now jarl-backup.timer
|
||||
systemctl list-timers jarl-backup.timer # when it runs next
|
||||
```
|
||||
|
||||
For example, as a daily timer or cron job:
|
||||
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
|
||||
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
|
||||
`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`
|
||||
show when the newest backup was made, and whether the last attempt
|
||||
failed.
|
||||
|
||||
A failed run exits non-zero, so it shows in `systemctl --failed` and
|
||||
`journalctl -u jarl-backup`.
|
||||
|
||||
What to keep and where is in `jarl.yaml`
|
||||
([configuration.md](configuration.md#backups)); when it runs is the
|
||||
timer's:
|
||||
|
||||
```yaml
|
||||
backup:
|
||||
dir: /var/backups/jarl
|
||||
retention: 30d # delete backups older than this (0 keeps them all)...
|
||||
keep_at_least: 7 # ...but always keep the newest 7
|
||||
stats: false # also back up the statistics database
|
||||
warn_after: 2d # dashboard warning when the newest backup is older
|
||||
```
|
||||
|
||||
The settings are read by `jarld backup` itself each time it runs, from the
|
||||
same file jarld uses, and checked the same way: a mistake there stops
|
||||
`jarld` from starting rather than quietly breaking the backups.
|
||||
|
||||
To run at a different time, override the timer, and keep `warn_after`
|
||||
above the new interval:
|
||||
|
||||
```sh
|
||||
# /etc/cron.daily/jarl-backup
|
||||
#!/bin/sh
|
||||
set -e
|
||||
install -d -o jarl -g jarl -m 0700 /var/backups/jarl
|
||||
sudo -u jarl jarld backup /var/backups/jarl/jarl-$(date +%F).db
|
||||
find /var/backups/jarl -name 'jarl-*.db' -mtime +30 -delete
|
||||
systemctl edit jarl-backup.timer
|
||||
# [Timer]
|
||||
# OnCalendar=
|
||||
# OnCalendar=*-*-* 02:30
|
||||
```
|
||||
|
||||
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
|
||||
`backup.dir`, create the directory (owned by `jarl`, mode 0700) and allow
|
||||
it:
|
||||
|
||||
```sh
|
||||
systemctl edit jarl-backup.service
|
||||
# [Service]
|
||||
# ReadWritePaths=/srv/backups/jarl
|
||||
```
|
||||
|
||||
Backups on the same disk don't survive the disk. Copy
|
||||
`/var/backups/jarl` elsewhere (rsync, restic, borg, your usual backup) as
|
||||
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
|
||||
```
|
||||
|
||||
Query statistics live in a separate database (`/var/lib/jarl/stats.db`).
|
||||
It isn't needed to restore DNS, so back it up only if you want to keep the
|
||||
history.
|
||||
DNS doesn't need it, so it is backed up only with `backup.stats: true`, as
|
||||
`stats-<UTC time>.db`.
|
||||
|
||||
The database contains password and token *hashes* and the TSIG secrets in
|
||||
the clear, so keep backups private.
|
||||
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.
|
||||
|
||||
### Restoring
|
||||
|
||||
`jarld restore` replaces the database with a backup. jarld must be
|
||||
stopped: the restore refuses to run while jarld holds the database (and
|
||||
jarld won't start while a restore runs).
|
||||
|
||||
```sh
|
||||
systemctl stop jarl
|
||||
install -o jarl -g jarl -m 0600 /var/backups/jarl/jarl-2026-10-01.db /var/lib/jarl/jarl.db
|
||||
rm -f /var/lib/jarl/jarl.db-wal /var/lib/jarl/jarl.db-shm
|
||||
systemctl start jarl
|
||||
jarl resync --dry-run # what differs between the restored database and the files
|
||||
jarl resync --force # write everything and reload named
|
||||
sudo jarld restore # list the backups, newest first
|
||||
sudo systemctl stop jarl
|
||||
sudo jarld restore latest # or: sudo jarld restore /var/backups/jarl/jarl-20261008T031512Z.db
|
||||
```
|
||||
|
||||
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. If you have secondaries, raise the serials after a restore:
|
||||
`jarl zone update ZONE --serial <higher>`.
|
||||
It checks the backup's integrity and that this Jarl can read it (a backup
|
||||
from a newer Jarl is refused), shows what the backup and the current
|
||||
database hold (zones, records, users, last change), and asks before
|
||||
changing anything (`--yes` skips the question). Then it:
|
||||
|
||||
* moves the current database aside as
|
||||
`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.
|
||||
|
||||
Then start jarld and bring named in line with the restored database:
|
||||
|
||||
```sh
|
||||
sudo systemctl start jarl
|
||||
jarl status # drift lists the files that differ
|
||||
jarl resync --dry-run # what would change
|
||||
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`.
|
||||
* **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.
|
||||
* **Statistics**: to restore `stats-<time>.db`, stop jarld and copy it over
|
||||
`/var/lib/jarl/stats.db` (owner `jarl`, mode 0640).
|
||||
* **On a new machine**: install Jarl, restore the backup, start jarld and
|
||||
run `jarl resync --force`; then adopt named as in
|
||||
[install.md](install.md) if this named doesn't include Jarl's
|
||||
configuration yet.
|
||||
|
||||
## Drift: files changed outside Jarl
|
||||
|
||||
@@ -143,6 +250,85 @@ Things that both servers need, such as ACLs, can be changed on both at once
|
||||
with a group (`jarl -s dns acl update ...`): the change is checked on both
|
||||
first and applied only if both accept it.
|
||||
|
||||
## Checking name servers and the delegation
|
||||
|
||||
Two checks ask other name servers about a zone over DNS. They only run when
|
||||
you ask: on the zone's **Name servers** tab in the web UI, or with the CLI.
|
||||
Nothing checks in the background.
|
||||
|
||||
### Are the other servers serving the current serial?
|
||||
|
||||
```
|
||||
$ jarl zone serials adbraun.lol
|
||||
adbraun.lol, primary, serial 2026100814 (served by named here)
|
||||
|
||||
SERVER ADDRESS FROM STATUS SERIAL TIME
|
||||
ns1.adbraun.lol 192.168.1.53 NS record this server 2026100814
|
||||
ns2.adbraun.lol 192.168.1.54 NS record BEHIND by 2 2026100812 1 ms
|
||||
```
|
||||
|
||||
It works for both sides of a zone transfer:
|
||||
|
||||
* **Primary zones**: asks the servers in the zone's NS records and its
|
||||
`also-notify` targets (the secondaries) which serial they serve, and
|
||||
compares it with what named on this server serves. A secondary that is
|
||||
behind hasn't picked up the latest change: check its logs, and that
|
||||
`allow-transfer` on this server lets it transfer the zone.
|
||||
* **Secondary and mirror zones**: asks the zone's `primaries` (following
|
||||
named `remote-servers` lists, with their ports and TSIG keys), to see
|
||||
whether this server's copy is up to date.
|
||||
|
||||
Name server addresses come from Jarl's own A/AAAA records in the zone's
|
||||
view where it has them, otherwise from a DNS lookup on the server. This
|
||||
server itself is recognised by its addresses and isn't asked. Statuses:
|
||||
*in sync*, *behind/ahead by N* (serials compare as in RFC 1982, so a
|
||||
wrapped serial counts as ahead), *doesn't serve the zone* (the server
|
||||
answers but isn't authoritative for it), *refused*, *no answer*, and *no
|
||||
route from here* (typically an IPv6 address on a server without IPv6,
|
||||
which isn't counted as a problem).
|
||||
|
||||
With views, other servers answer from the view that matches this server's
|
||||
address (or TSIG key), exactly as they do for its zone transfers and
|
||||
NOTIFYs; queries to primaries and also-notify targets that name a key are
|
||||
signed with it.
|
||||
|
||||
`jarl zone serials` exits with 2 when a server isn't serving the current
|
||||
serial, so it can run from cron or a monitoring system.
|
||||
|
||||
### Does the internet see the zone the way Jarl does?
|
||||
|
||||
For a zone published on the internet, the parent zone (`lol.` for
|
||||
adbraun.lol, set through your registrar) must delegate it to the right name
|
||||
servers, with glue addresses for name servers inside the zone.
|
||||
|
||||
```
|
||||
$ jarl zone delegation adbraun.lol --view external
|
||||
Zone: adbraun.lol
|
||||
Parent zone: lol. (found through 127.0.0.53:53)
|
||||
a.nic.lol and 3 more: delegate to ns1.adbraun.lol, ns2.adbraun.lol
|
||||
NS at the parent: ns1.adbraun.lol, ns2.adbraun.lol
|
||||
NS in the zone: ns1.adbraun.lol, ns2.adbraun.lol
|
||||
Glue ns1.adbraun.lol: 203.0.113.53 (matches)
|
||||
...
|
||||
ok: The parent delegates adbraun.lol to the same name servers as its NS records: ...
|
||||
ok: Every delegated name server answers for the zone with the current serial.
|
||||
```
|
||||
|
||||
It finds the parent zone through the server's resolver (or `--resolver
|
||||
9.9.9.9`), asks each of the parent's servers directly, and reports:
|
||||
|
||||
* name servers the parent delegates to that the zone's NS records don't
|
||||
list, and the other way round;
|
||||
* glue that is missing, or differs from the zone's A/AAAA records;
|
||||
* delegated servers that don't serve the zone (a *lame delegation*), don't
|
||||
answer, or serve another serial.
|
||||
|
||||
A zone that is only used on your own network isn't delegated at all, and
|
||||
the check says so in a single note, not as a problem; it also exits with 0.
|
||||
It exits with 2 only for errors (a lame delegation, missing glue). With
|
||||
split DNS, check the zone in the view the internet sees (usually
|
||||
`external`).
|
||||
|
||||
## Taking Jarl out of the picture
|
||||
|
||||
The generated files are plain BIND configuration. To stop using Jarl,
|
||||
@@ -166,3 +352,14 @@ rndc reconfig
|
||||
```
|
||||
* The audit log records failed logins as `auth.login_failed` /
|
||||
`auth.login_blocked`, and rejected tokens as `auth.token_failed`.
|
||||
* Backups: `.backup.stale` and `.backup.last_error` in `jarl status -o
|
||||
json` (administrators), and `systemctl --failed` for the timer's runs:
|
||||
```sh
|
||||
sudo jarl status -o json | jq -e '.backup.stale == false and (.backup.last_error // "") == ""'
|
||||
```
|
||||
* Secondaries: `jarl zone serials ZONE` exits with 2 when one isn't serving
|
||||
the current serial.
|
||||
* Tokens: jarld logs a warning once a day for every token that expires
|
||||
within 14 days (`journalctl -u jarl | grep 'token expires'`), the
|
||||
dashboard lists them, and `jarl` prints a warning on stderr whenever it
|
||||
uses one, so a certbot hook's log shows it well before renewals fail.
|
||||
@@ -114,6 +114,25 @@ jarl token revoke 12
|
||||
In the web UI: Account → API tokens (your own) and Users → *user* →
|
||||
Tokens (admin).
|
||||
|
||||
### Expiring tokens
|
||||
|
||||
A token with an expiry stops working at that moment. So that it doesn't
|
||||
catch you out (a certbot token that expired breaks renewals silently, until
|
||||
the certificate itself expires), Jarl warns from **14 days before**:
|
||||
|
||||
* the dashboard lists the tokens that expire soon or expired in the last 14
|
||||
days (administrators see everyone's, others their own), and token lists
|
||||
mark them *soon* or *expired*;
|
||||
* jarld logs a warning once a day for each of them;
|
||||
* every response to such a token carries a `Jarl-Token-Expires` header,
|
||||
and the `jarl` CLI prints a warning on stderr when it sees it, so the
|
||||
logs of the script using the token show it;
|
||||
* `jarl token list --all --expiring` lists them (`--expiring 60d` looks
|
||||
further ahead).
|
||||
|
||||
Rotating the token (below) gives it a new lifetime, and keeps its
|
||||
permissions.
|
||||
|
||||
### Rotating a token
|
||||
|
||||
Rotating gives a token a **new secret** and changes nothing else: it keeps
|
||||
|
||||
Reference in new issue
Block a user