Multi-server CLI, more reverse zones, and a fix for new view directories

- CLI: name several servers and groups in the config file; select them
  with -s/--server NAME|GROUP|all|URL or JARL_SERVER. A command aimed at
  several servers is dry-run on all of them first and only runs if every
  one accepts it. Named servers always use their own token.
- Reverse zones: /32 and /128 get single-address zones; networks between
  octet or nibble boundaries (e.g. a /22) are covered by several zones
  created together, from the CLI and the web UI.
- Fix: under jarl.service's RestrictSUIDSGID, creating a view directory
  failed on chmod with the setgid bit; fall back to the permission bits.
- Web UI: the review/live switch is a small header button, and the
  pending-changes bar only shows when there are pending changes.
- TSIG key wording; the keys page points to restricted API tokens.
- Docs: several servers (cli.md), a secondary server (operations.md),
  tokens vs TSIG keys (permissions.md); CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-08 08:12:03 +00:00
1 parent a7d389be27
commit 785b0006f9
22 files changed
+1375 -185

No files matched your search

+60 -1
View File
@@ -14,7 +14,7 @@ jarl whoami
| Setting | Flag | Environment | Config file key |
|---------|------|-------------|-----------------|
| Server URL | `--server` | `JARL_SERVER` | `server` |
| Server URL, or a server or group name (below) | `-s/--server` | `JARL_SERVER` | `server` (`default` for a name) |
| API token (`@FILE` reads it from a file) | `--token` | `JARL_TOKEN` | `token` |
| Skip TLS verification | `--insecure` | | `insecure` |
| Extra trusted CA (PEM) | `--ca-cert` | | `ca_cert` |
@@ -27,6 +27,55 @@ file is `~/.config/jarl/config.yaml` (mode 0600), or `--config` /
Get tokens from the web UI (Account → API tokens), from
`jarl token create`, or from `jarld init --token` on the server.
### Several servers and groups
With more than one Jarl (say a primary and a secondary, each with its own
jarld), give each a name; group them to run a command on several at once:
```sh
jarl config server add ns1 https://ns1.adbraun.lol:8053 # asks for ns1's token
jarl config server add ns2 https://ns2.adbraun.lol:8053
jarl config group set dns ns1 ns2
jarl config use ns1 # the default when --server is left out
jarl -s ns2 zone list # or JARL_SERVER=ns2
jarl -s dns acl update homelab --add 10.0.20.0/24
jarl -s all status # every named server
jarl -s ns2 config set token # change ns2's token
```
The file then looks like this:
```yaml
servers:
ns1: {url: "https://ns1.adbraun.lol:8053", token: jarl_...}
ns2: {url: "https://ns2.adbraun.lol:8053", token: "@/home/adam/.config/jarl/ns2.token"}
groups:
dns: [ns1, ns2]
default: ns1
```
`--server` takes a server name, a group (groups may contain groups),
`all`, a comma separated list of those, or a URL. A named server always uses
its own token; `--token` and `JARL_TOKEN` only fill in for servers without
one, so a token is never sent to the wrong server. The plain `server:` and
`token:` keys still work and are used when nothing is selected.
**How a command runs on a group.** It is first run on every server as a dry
run (in parallel). If any server rejects it (for example a record in a zone
that is a secondary there, an ACL that already exists, or a token without the
permission), the errors are shown and *nothing is changed anywhere*. Only
when it would succeed everywhere does it run for real, one server after
another, with a `== ns1 (URL) ==` header before each server's output. A
read-only command simply shows each server's answer. If a server still fails
in the real run (it went down in between, say), jarl stops and says which
servers already applied the change.
Commands that can't be dry-run (`token create`, `user ...`, `changeset
create/discard/revert`) refuse to run on a group, and `--changeset` can't be
combined with one, since changeset IDs belong to a single server. Headers go
to stderr with `-o json` or `-o yaml`, so stdout holds just the documents.
## Global flags
| Flag | Meaning |
@@ -65,6 +114,8 @@ jarl zone list [--view V] [--type primary]
jarl zone show adbraun.lol
jarl zone create lab.adbraun.lol --ns ns1.adbraun.lol.
jarl zone create 192.168.20.0/24 # reverse zone from a network
jarl zone create 10.0.8.0/22 # four zones, 8-11.0.10.in-addr.arpa
jarl zone create 10.0.10.100/32 # a zone for just that address
jarl zone create adbraun.lol --view external --clone-from adbraun.lol --clone-view internal
jarl zone create partner.example --type secondary --primary "192.0.2.53 key xfer"
jarl zone create consul --type forward --forwarder "127.0.0.1 port 8600"
@@ -75,6 +126,14 @@ jarl zone file adbraun.lol # the generated zone file
jarl zone import adbraun.lol db.adbraun.lol [--replace] [--use-soa]
```
Reverse names follow the dots (the nibbles in IPv6), so a reverse zone
covers a /8, /16 or /24 (a multiple of /4 for IPv6). A network in between is
covered by several zones, created together in one changeset: a /22 by four
/24 zones (up to 16 zones). A /32 (or /128) gets a zone of its own that
answers for that one address: useful when another server, such as your
router, answers for the rest of the /24. IPv4 networks smaller than a /24
(/25 to /31) live in the enclosing /24's zone.
`zone import` adds the records of a zone file (`-` reads stdin). With
`--replace` the zone is made equal to the file, so records that aren't in it
are deleted. Try it with `--dry-run` first. `--option "NAME VALUE"` on
+5 -3
View File
@@ -219,6 +219,8 @@ journal through jarld's log.
zones. Use the API (for example `jarl acme`) instead of nsupdate.
* **DNSSEC signing**: not managed yet. The zone directory is
named-writable, so `dnssec-policy` can be added later.
* **Several servers**: Jarl manages the named on its own host. Secondaries
elsewhere get zones by normal AXFR/IXFR (`allow-transfer`, `also-notify`,
TSIG keys).
* **Several servers from one jarld**: each jarld manages the named on its
own host. A second server runs its own jarld with secondary zones, which
copy the primary's zones by normal AXFR/IXFR (see "A secondary server" in
[operations.md](operations.md)). The CLI can name both and run a command
on both at once ([cli.md](cli.md#several-servers-and-groups)).
+30
View File
@@ -105,6 +105,36 @@ Start named, then apply again.
| Lost every admin account | `sudo -u jarl jarld user add --admin rescue` |
| Need a token without the web UI | `sudo -u jarl jarld user token admin cli` |
## A secondary server
A second name server runs its own named and jarld; its zones are of type
`secondary` and copy the primary's records by zone transfer, so you only
ever edit records on the primary. With both named in the CLI configuration
([cli.md](cli.md#several-servers-and-groups)):
```sh
# 1. One TSIG key with the same secret on both servers.
jarl -s ns1 tsig create xfer # prints the secret
jarl -s ns2 tsig create xfer --secret 'THE-SECRET'
# 2. On the primary: let the key transfer the zone, and list ns2 as a
# name server (named sends NOTIFY to the zone's NS records).
jarl -s ns1 option set allow-transfer '{ key xfer; }' --zone adbraun.lol
jarl -s ns1 record add adbraun.lol @ NS ns2.adbraun.lol.
# 3. On the secondary: the zone, copied from the primary with the key.
jarl -s ns2 zone create adbraun.lol --type secondary --primary "192.0.2.53 key xfer"
```
If ns2 isn't one of the zone's NS records, add `also-notify { ADDRESS; };`
on the primary so it learns about changes quickly. With views, each view's
zones are transferred separately; match the secondary's requests to the
right view, for example with `match-clients { key xfer; ... };`.
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.
## Taking Jarl out of the picture
The generated files are plain BIND configuration. To stop using Jarl,
+6
View File
@@ -8,6 +8,12 @@
| CLI, scripts, certbot | API token: `Authorization: Bearer jarl_...` | Shown once at creation, stored hashed. Optional expiry. Revocable. Last use time and IP are recorded. |
| Root on the server | `jarld` subcommands (`init`, `user`, `import`, `adopt`, `check`, `resync`, `backup`) | Work directly on the database. They are for bootstrapping and recovery, and are audited as "local". |
API tokens are how you give a script, a tool or a person limited access:
a token restricted with grants (below) can, for example, only change TXT
records under `_acme-challenge` in one zone. TSIG keys, on the TSIG keys page,
are something else: shared secrets that BIND servers use to authenticate
each other (zone transfers, NOTIFY); they don't give access to Jarl.
Failed logins are rate limited: 8 per username and 20 per client IP within
15 minutes. Failed token attempts count against the IP too. Every attempt
is in the audit log.