# 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, and `jarl completion bash|zsh|fish` prints shell completion. ## Setup ```sh 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 | `--server` | `JARL_SERVER` | `server` | | 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 `~/.config/jarl/config.yaml` (mode 0600), or `--config` / `JARL_CLI_CONFIG`. `jarl config show` prints it with the token masked. Get tokens from the web UI (Account → API tokens), from `jarl token create`, or from `jarld init --token` on the server. ## 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, ...) | | 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 ```sh 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 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] ``` `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. ## Records ```sh 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] ``` 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. `--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: ```yaml # 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]} ``` ```sh 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 ```sh 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 ```sh 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 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) ```sh 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 create laptop --expires 365d jarl token create backup-script --grant "*:read" jarl token create acme --user certbot --role acme 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 check # validation + named-checkconf of the current configuration jarl resync [--force] [--dry-run] # rewrite every file from the database ``` Grant syntax and roles are in [permissions.md](permissions.md). ## ACME DNS-01 ```sh jarl acme set www.adbraun.lol # TXT at _acme-challenge.www.adbraun.lol jarl acme set '*.adbraun.lol' # wildcard + apex use the same name jarl acme unset www.adbraun.lol [] jarl acme certbot-auth / certbot-cleanup # certbot manual hooks ``` See [acme.md](acme.md) for certbot, acme.sh and a dedicated ACME zone. ## Scripting tips * `-o json` gives full API objects. Pipe them to `jq`. * `-q` with `create` commands prints just the ID or token. * Check `$?` against the exit code table. * `jarl record delete ... --missing-ok` succeeds when nothing matches, which keeps cleanup scripts idempotent. `jarl record set` is idempotent by nature.