The view shown in the zone list and on the zone page now links to that
view. The zone page and changeset page get breadcrumbs, and the zone list
keeps its filters in the address so going back restores them. Views can
be given a colour (Views > Edit, `jarl view color`, PUT /views/{id}/color)
so zones of the same view are easy to spot; the colour is UI-only, kept
outside changesets, and needs no deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
17 KiB
The jarl command line client
jarl talks to jarld's REST API with an API token. It runs on the DNS server
or on any machine that can reach jarld. Every command has --help with
examples.
Shell completion
make install and the Debian package install bash completion for jarl
and jarld; it works in new shells (it needs the bash-completion
package, which Debian and Ubuntu install by default). Elsewhere, or for
zsh and fish, jarl completion bash|zsh|fish prints the script; for
example jarl completion bash > ~/.local/share/bash-completion/completions/jarl.
Besides commands and flags, Tab completes:
-s/--serverand theconfigcommands: the server and group names in your configuration file (andall; after a comma, the next name);ZONE,VIEW,ACL,USERand TSIGKEYarguments, and the--view,--zoneand--userflags: names from the server, asked with your token. A zone in several views is offered once, with its views shown. If the server can't be reached in 3 seconds, nothing is offered;- record
TYPEs, with a short description of each; -o/--output:table,json,yaml.
Setup
On the DNS server the CLI comes with Jarl. To use it from another machine,
install just the jarl-cli package there, or copy the static jarl binary
(install.md).
jarl config set server https://ns1.adbraun.lol:8053
jarl config set token # paste the token at the prompt
jarl whoami
| Setting | Flag | Environment | Config file key |
|---|---|---|---|
| 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 |
|
| Changeset to stage into | -C/--changeset |
JARL_CHANGESET |
Flags win over the environment, and the environment wins over the file. The
file is jarl/config.yaml in the user's configuration directory, or
--config / JARL_CLI_CONFIG:
| System | File |
|---|---|
| Linux and other Unix | ~/.config/jarl/config.yaml ($XDG_CONFIG_HOME/jarl/config.yaml if set) |
| macOS | ~/Library/Application Support/jarl/config.yaml |
| Windows | %AppData%\jarl\config.yaml (C:\Users\<you>\AppData\Roaming\jarl\config.yaml) |
It is written with mode 0600 (on Windows, the AppData folder is already
private to the user). jarl config show prints its path and the settings,
with the token masked. On Windows and macOS there is no local jarld, so
always set a server.
Get tokens from the web UI (Account → API tokens), from
jarl token create, or from jarld init --token on the server.
On the server itself
On the DNS server, root needs no setup at all:
sudo jarl status
sudo jarl record add adbraun.lol www A 192.0.2.10
With no server configured, the CLI talks to jarld through its local unix
socket, /run/jarl/jarl.sock, where root and the jarl user are
administrators without a token (permissions.md).
The history and audit log show such changes as made by root via
socket.
-s local(orJARL_SERVER=local) uses the socket even when servers are configured, andlocalcan be part of a list (-s local,ns2).localis reserved and can't be a configured server's name.JARL_SOCKETpoints at another socket path.- A token given with
--tokenorJARL_TOKENis sent over the socket as well, and then its permissions apply. - Other users get a clear error ("only root and the jarl user can use jarld's local socket"); they configure a server and token as above.
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:
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:
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 |
|---|---|
-o table|json|yaml |
Output format. JSON and YAML have the API's full objects, for scripts. |
-n, --dry-run |
Validate and show the diff without changing anything. |
-C, --changeset ID |
Stage the change into an open changeset instead of applying it. |
--force |
Overwrite BIND files that were changed outside Jarl (drift). |
-q, --quiet |
Print only errors, or just the ID where a command creates something. |
Exit codes
| Code | Meaning |
|---|---|
| 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 |
| 6 | Invalid request, or rejected by validation (e.g. a CNAME conflict, or named-checkconf found an error) |
| 7 | Server error, or BIND failed: a tool couldn't run, or named didn't load the change (it was rolled back) |
Validation errors are printed with the offending zone and view:
$ jarl record add adbraun.lol www A 192.168.1.99
error: www.adbraun.lol has a CNAME, so it can't have any other records (found A); a CNAME must be the only record at its name (RFC 1034) [zone adbraun.lol]
$ echo $?
6
Zones
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"
jarl zone update adbraun.lol --ttl 3600 --contact hostmaster@adbraun.lol --refresh 7200
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 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
zone create sets any zone option in the same step.
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.
Records
jarl record list adbraun.lol [--name www] [-t A] [--search nas]
jarl record add adbraun.lol nas A 192.168.1.20 --ptr --comment "synology"
jarl record add adbraun.lol @ MX 10 mail
jarl record add adbraun.lol @ TXT "v=spf1 mx -all" # plain text, quoting is automatic
jarl record add adbraun.lol _sip._tcp SRV 0 5 5060 sip
jarl record add adbraun.lol @ CAA 0 issue "letsencrypt.org"
jarl record set adbraun.lol api A 192.168.1.30 192.168.1.31 --ttl 60 # replace the RRset
jarl record update 1234 --data 192.168.1.21 --ptr # IDs from 'record list'
jarl record update 1234 --ttl default
jarl record delete adbraun.lol www A # every A record of www
jarl record delete adbraun.lol www A 192.168.1.10 # just this one
jarl record delete --id 1234 [--ptr]
# reverse zones: name records by address; the host name needs no final dot
jarl record add 0.10.10.in-addr.arpa 10.10.0.20 PTR nas.adbraun.lol
Names are relative to the zone (@ is the apex) unless they end with a
dot. Data uses zone file syntax, except that TXT/SPF take plain text: Jarl
adds the quotes and splits long values into 255-byte strings. In reverse zones the name can be the address
(10.10.0.20), and a PTR target such as nas.adbraun.lol is read as the full
host name (a bare nas is refused, since it would mean
nas.0.10.10.in-addr.arpa.). --ptr
keeps the PTR record in the reverse zone in step (when that zone is managed
by Jarl). Every record type BIND supports can be used. Jarl knows the
syntax of the common ones and checks them strictly, and the rest are
validated by parsing.
Batches: jarl apply -f
Several changes that must succeed or fail together:
# move-web.yaml
description: move the web server to the new VM
changes:
- add: {zone: adbraun.lol, name: web2, type: A, data: 192.168.1.40, ttl: 300, ptr: true}
- delete: {zone: adbraun.lol, name: www, type: CNAME}
- add: {zone: adbraun.lol, name: www, type: CNAME, data: web2}
- set: {zone: adbraun.lol, name: api, type: A, data: [192.168.1.41, 192.168.1.42], ttl: 60}
- update: {id: 1235, comment: "old web server, remove in November"}
- delete: {id: 1240}
- op: acl.update # any API operation (see api.md)
id: 3
data: {elements: [192.168.1.0/24, 10.10.0.0/16]}
jarl apply -f move-web.yaml --dry-run # check and show the diff
jarl apply -f move-web.yaml
The whole file is validated as one changeset, at its end state, so the
order of the entries doesn't matter (the CNAME swap above works either way
round). Add view: NAME to entries
when a zone exists in several views. JSON works as well as YAML, and -f -
reads standard input.
Changesets: stage, review, apply
export JARL_CHANGESET=$(jarl changeset create "renumber lab" -q)
jarl record set lab.adbraun.lol gw A 10.20.0.1
jarl record set lab.adbraun.lol nas A 10.20.0.10
jarl changeset diff $JARL_CHANGESET # validation + named-checkconf + diff
jarl changeset remove $JARL_CHANGESET 2 # drop operation #2
jarl changeset apply $JARL_CHANGESET
unset JARL_CHANGESET
jarl changeset list [--status open|applied|discarded] [--mine]
jarl changeset show 42
jarl changeset discard 43
jarl history [--limit 50]
jarl changeset revert 42 -q | xargs jarl changeset apply # undo an applied changeset
Changesets staged in the web UI show up here too, and the other way round.
Views, ACLs, TSIG keys, options, settings
jarl view create internal --match-clients homelab,localhost --adopt-zones
jarl view create external --match-clients any
jarl view order internal external
jarl view update external --match-clients "!homelab,any"
jarl view delete old [--cascade]
jarl view color internal green # web UI colour; "default" resets it
jarl acl create homelab 192.168.1.0/24 10.10.0.0/16 localhost
jarl acl update homelab --add 10.20.0.0/16 --remove 10.10.0.0/16
jarl acl update homelab --rename lan # references are updated
jarl acl delete lan # refused while in use
jarl tsig create xfer [--algorithm hmac-sha512]
jarl tsig show xfer --named-conf # paste into the secondary's named.conf
jarl tsig rotate xfer
jarl tsig delete xfer
jarl option list [--view V | --zone Z | --toplevel | --all]
jarl option set recursion yes
jarl option set allow-recursion '{ homelab; localhost; }'
jarl option set forwarders '{ 1.1.1.1; 9.9.9.9; }'
jarl option set allow-transfer '{ key xfer; }' --zone adbraun.lol
jarl option set response-policy '{ zone "rpz.local"; }' --view internal
jarl option set logging '{ channel q { file "/var/log/named/query.log" versions 3 size 20m; }; category queries { q; }; }' --toplevel
jarl option unset forwarders
jarl option describe # every option allowed at this scope
jarl option describe allow-query-cache # grammar and ARM description
jarl settings show
jarl settings set --ns ns1.adbraun.lol.,ns2.adbraun.lol. --contact hostmaster@adbraun.lol --ttl 3600
Users, tokens, audit (administration)
jarl user list
jarl user create alice --role dns-editor # asks for a password
jarl user create ci --no-password --grant "records:write zones=lab.adbraun.lol"
jarl user update alice --admin
jarl user update alice --password # set a new password
jarl user update bob --disabled # logs bob out, blocks his tokens
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
jarl token rotate laptop --save # new secret, same token and grants
jarl token rotate --current --save --grace 10m # the token this CLI uses
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, 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.
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
jarl stats status
jarl stats summary --since 7d
jarl stats top names | clients | nxdomain | servfail | refused [--since 24h] [--limit 20]
jarl stats timeline --since 6h [--key rcode:NXDOMAIN] [--table]
jarl stats heatmap --since 28d
jarl stats breakdown qtype | rcode | view | zone | answer | rtt
Statistics must be enabled in jarl.yaml, and -o json exports the data.
See stats.md.
ACME DNS-01
jarl acme set www.adbraun.lol <validation> # TXT at _acme-challenge.www.adbraun.lol
jarl acme set '*.adbraun.lol' <value1> <value2> # wildcard + apex use the same name
jarl acme unset www.adbraun.lol [<validation>]
jarl acme certbot-auth / certbot-cleanup # certbot manual hooks
See acme.md for certbot, acme.sh and a dedicated ACME zone.
Scripting tips
-o jsongives full API objects. Pipe them tojq.-qwithcreatecommands prints just the ID or token.- Check
$?against the exit code table. jarl record delete ... --missing-oksucceeds when nothing matches, which keeps cleanup scripts idempotent.jarl record setis idempotent by nature.