Jarl is now an ACME client (lego's core, no DNS providers). It answers DNS-01 challenges in a zone it serves, or through Porkbun's DNS API while Porkbun serves the domain; "auto" decides from the domain's public name servers at every renewal, and a CNAME on _acme-challenge is followed. Certificates are used for jarld's HTTPS (served without a restart; each port speaks HTTPS and plain HTTP, plain HTTP from loopback and trusted proxies still served) and/or written for named's DNS over TLS/HTTPS, with rndc reconfig on renewal. Renewal follows the CA's ARI window or two thirds of the lifetime, with back-off. Certificates you renew yourself can be used too. Certificates page and dashboard card, jarl cert, /api/v1/certificates, Prometheus gauges, docs/certificates.md, and an e2e section against Pebble. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
583 lines
28 KiB
Markdown
583 lines
28 KiB
Markdown
# 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/--server` and the `config` commands: the server and group names in
|
|
your configuration file (and `all`; after a comma, the next name);
|
|
* `ZONE`, `VIEW`, `ACL`, `USER` and TSIG `KEY` arguments, and the `--view`,
|
|
`--zone` and `--user` flags: 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 `TYPE`s, 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](install.md#just-the-cli-on-another-machine)).
|
|
|
|
```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, 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 (and the default for `jarl changeset diff/apply/...`) | `-C/--changeset` | `JARL_CHANGESET` (checked: open and yours, see [Changesets](#changesets-stage-review-apply)) | |
|
|
|
|
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:
|
|
|
|
```sh
|
|
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](permissions.md#the-local-socket)).
|
|
The history and audit log show such changes as made by `root` via
|
|
`socket`.
|
|
|
|
* `-s local` (or `JARL_SERVER=local`) uses the socket even when servers
|
|
are configured, and `local` can be part of a list (`-s local,ns2`).
|
|
`local` is reserved and can't be a configured server's name.
|
|
* `JARL_SOCKET` points at another socket path.
|
|
* A token given with `--token` or `JARL_TOKEN` is 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:
|
|
|
|
```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 |
|
|
|------|---------|
|
|
| `-o table\|json\|yaml` | Output format. JSON and YAML have the API's full objects, for scripts. |
|
|
| `-o id`, `-o field=NAME[,NAME...]` | Just the IDs, or just those fields, one line per object (fields separated by tabs). See [Scripting tips](#scripting-tips). |
|
|
| `-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). |
|
|
| `--accept-risk` | Confirm a change that opens a public view up, such as an open resolver ([exposure.md](exposure.md)). Without it such a change stops with exit 5. |
|
|
| `-q, --quiet` | Print only errors, or just the IDs: of what a command creates, or what `show` and `list` find (like `-o id`). |
|
|
|
|
## Exit codes
|
|
|
|
| Code | Meaning |
|
|
|------|---------|
|
|
| 0 | Success |
|
|
| 1 | Other error (bad arguments, connection failure, ...) |
|
|
| 2 | A check found problems (`zone serials`, `zone delegation`, `zone dnssec check-ds`, `zone registrar status`), or a step can't be taken yet (`zone dnssec disable` before turning off has finished); the 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
|
|
|
|
```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 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 disable lab.adbraun.lol [--view V] # named stops serving it; kept (and editable) in Jarl
|
|
jarl zone enable lab.adbraun.lol [--view V]
|
|
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?
|
|
```
|
|
|
|
**Disabling a zone** leaves it out of named's configuration but keeps it in
|
|
Jarl: records, options and history, which can still be edited. Its names
|
|
are then answered by recursion where the view offers it (the internet's
|
|
version of the zone, if any), and REFUSED elsewhere; `disable` says which.
|
|
Enabling it serves it as it is then. A signed zone can't be disabled (turn
|
|
signing off first), nor a zone another view shares with `in-view`.
|
|
|
|
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](operations.md#checking-name-servers-and-the-delegation).
|
|
|
|
### DNSSEC and the registrar
|
|
|
|
```sh
|
|
jarl zone dnssec status adbraun.lol --view external # keys, DS record, what's next
|
|
jarl zone dnssec enable adbraun.lol --view external [--policy NAME]
|
|
jarl zone dnssec check-ds adbraun.lol --view external # has the parent got the DS? tells named
|
|
jarl zone dnssec disable adbraun.lol --view external # step 1: insecure; run again to finish
|
|
jarl zone dnssec wipe adbraun.lol --view external [--force] # off at once, keys deleted
|
|
|
|
jarl zone registrar status adbraun.lol --view external # Porkbun: NS, glue, DS vs the zone
|
|
jarl zone registrar sync-glue adbraun.lol --view external [--dry-run]
|
|
jarl zone registrar set-ns adbraun.lol --view external [NS...] [--dry-run]
|
|
jarl zone registrar publish-ds adbraun.lol --view external [--key TAG]
|
|
jarl zone registrar remove-ds adbraun.lol 51126 --view external
|
|
```
|
|
|
|
[dnssec.md](dnssec.md) explains the steps. `enable` is safe at any time:
|
|
validation only starts once the zone's DS record is at the parent, and
|
|
`status` says when to publish it. `disable` never makes a zone unsigned
|
|
while the parent may still have its DS: its first run switches to BIND's
|
|
insecure policy, and it finishes (exit 0) only when named is done (exit 2
|
|
before). `wipe` switches off at once and deletes the keys, and refuses
|
|
while the parent zone has a DS record for the zone; `--force` wipes when the
|
|
parent can't be checked.
|
|
|
|
The `registrar` commands need a Porkbun API key in jarl.yaml and an
|
|
administrator. Each change checks first that it won't break the domain and
|
|
is refused otherwise: `--dry-run` shows the checks, `--force` skips them.
|
|
(Here `--force` means that; elsewhere it overwrites files changed outside
|
|
Jarl.)
|
|
|
|
## Records
|
|
|
|
```sh
|
|
jarl record list adbraun.lol [--name www] [-t A] [--search nas] [--disabled]
|
|
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]
|
|
jarl record disable adbraun.lol www A 192.168.1.10 --ptr # out of the zone, kept in Jarl
|
|
jarl record enable adbraun.lol www A 192.168.1.10 --ptr # back again
|
|
jarl record disable --id 1234 # by ID; same for enable
|
|
|
|
# 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
|
|
|
|
# a zone in several views (split horizon): the same change in each
|
|
jarl record add adbraun.lol mx2 A 203.0.113.5 --view internal,external
|
|
jarl record delete adbraun.lol old A --view all -y
|
|
```
|
|
|
|
**Several views.** When a zone exists in more than one view, the record
|
|
commands that change records (`add`, `set`, `delete`, `disable`,
|
|
`enable`) take `--view a,b` or `--view all` (every view the zone is in).
|
|
The changes are applied together as one changeset: if one view refuses
|
|
(the zone isn't in it, a CNAME conflict, ...), no view is changed. With
|
|
`--changeset` they are staged there instead. Keep split-horizon data that
|
|
differs per view (an internal address, say) to one view at a time.
|
|
|
|
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.
|
|
|
|
**Disabling** a record takes it out of the zone without deleting it: it
|
|
keeps its ID, TTL and comment, the zone file lists it as a comment
|
|
(`; disabled: www IN A ...`), and `enable` puts it back exactly as it
|
|
was. `record list` marks disabled records `[disabled]`, and `zone show`
|
|
counts them. The zone is validated as it is served: you can't disable its
|
|
last NS record, and enabling a record is checked like adding it (a CNAME
|
|
added at the same name in the meantime stops it). `record set` enables a
|
|
disabled record whose value it is given, and leaves other disabled
|
|
records alone.
|
|
|
|
## 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}
|
|
- disable: {zone: adbraun.lol, name: old, type: A} # or {id: ...}; enable: likewise
|
|
- 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 # validation + named-checkconf + diff
|
|
jarl changeset remove 2 # drop operation #2
|
|
jarl changeset apply
|
|
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
|
|
```
|
|
|
|
`show`, `diff`, `apply`, `discard` and `remove` work on the changeset you
|
|
name, else the one in `-C`, else the one in `JARL_CHANGESET`.
|
|
|
|
A changeset from `JARL_CHANGESET` is checked before anything uses it,
|
|
because an exported ID easily outlives its changeset, and IDs keep growing
|
|
(every change, live ones included, gets the next one), so a stale or
|
|
mistyped ID can later name a changeset that isn't yours:
|
|
|
|
- Staging into it, and `diff`, `apply`, `discard` and `remove` on it, need
|
|
it to be open and yours. Otherwise the command stops and says why
|
|
(`there is no changeset 99`, `changeset 42 is applied`, `changeset 57 is
|
|
certbot's`) and what to do. Commands that only read (`jarl zone list`)
|
|
and `jarl changeset create` don't care, so you can always start a new one.
|
|
- Each command says on stderr which changeset it uses: `changeset 42 (renumber
|
|
lab, 2 operations) from JARL_CHANGESET` (not with `-q`).
|
|
- After `apply` or `discard`, a reminder to `unset JARL_CHANGESET`: the CLI
|
|
can't change your shell's environment, and later changes would fail to
|
|
stage.
|
|
|
|
`-C ID` and an ID argument are used as given, without these checks (jarld
|
|
still refuses a changeset that isn't open, or another user's unless you are
|
|
an administrator).
|
|
|
|
Changesets staged in the web UI show up here too, and the other way round.
|
|
|
|
## Internet exposure
|
|
|
|
```sh
|
|
jarl exposure # public views, findings, fixes, rate limiting
|
|
jarl exposure fix external exposure_rate_limit # rate limiting, log-only first
|
|
jarl exposure fix external exposure_rate_limit_log_only # then start enforcing
|
|
jarl exposure fix server exposure_version
|
|
jarl exposure ack external security_open_resolver --reason "public resolver, monitored"
|
|
jarl exposure unack external security_open_resolver
|
|
jarl view update external --exposure public|private|auto
|
|
```
|
|
|
|
See [exposure.md](exposure.md).
|
|
|
|
## Certificates
|
|
|
|
```sh
|
|
jarl cert check --domain ns1.adbraun.lol # where each challenge would go, and why
|
|
jarl cert create ns1 --domain ns1.adbraun.lol --use web --use dns # obtained at once; waits
|
|
jarl cert create wild -d adbraun.lol -d '*.adbraun.lol' --use dns --key-type ec384
|
|
jarl cert create own --cert-file /etc/ssl/ns1/fullchain.pem --key-file /etc/ssl/ns1/privkey.pem --use web
|
|
jarl cert list
|
|
jarl cert show ns1
|
|
jarl cert issue ns1 # renew now (--no-wait to return at once)
|
|
jarl cert update ns1 --challenge porkbun # auto, jarl or porkbun
|
|
jarl cert update ns1 --use dns # --use and --domain replace the list (--use none)
|
|
jarl cert delete ns1
|
|
jarl cert account # the ACME account (set in jarl.yaml)
|
|
```
|
|
|
|
Administrators only. See [certificates.md](certificates.md).
|
|
|
|
## 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 view color internal green # web UI colour; "default" resets it
|
|
jarl view key external [--named-conf] # the key that selects the view (dig -k), docs/views.md
|
|
|
|
jarl acl create homelab 192.168.1.0/24 10.10.0.0/16 localhost
|
|
jarl acl create dhcp 10.0.10.101-10.0.10.199 2001:db8::100-2001:db8::1ff # ranges (concepts.md)
|
|
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 forwarders 1.1.1.1 9.9.9.9 # the same: braces are added to a list
|
|
jarl option set allow-transfer 'key xfer; 10.0.10.53' --zone adbraun.lol
|
|
jarl option set allow-query '10.0.20.1-10.0.20.99; localhost' --view internal
|
|
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
|
|
```
|
|
|
|
Options that end in a list (`allow-*`, `forwarders`, `listen-on`, ...) may
|
|
be given without the braces: elements are separated by `;`, `,` or spaces,
|
|
and `key NAME` and `port N` stay with their element. Options are always
|
|
listed in one fixed order (see [concepts.md](concepts.md)).
|
|
|
|
```sh
|
|
|
|
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 bob's tokens
|
|
jarl user update admin --rename adam # rename (e.g. the first account, to your own name)
|
|
jarl user totp reset alice # turn off alice's two-factor login (lost phone)
|
|
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 user list' and 'jarl user show' include two-factor login (2FA: on,
|
|
# off, or MISSING when totp.require applies and it isn't set up). Users set
|
|
# it up themselves in the web UI (Account); the CLI's tokens never need a
|
|
# code (permissions.md#two-factor-login).
|
|
|
|
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](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
|
|
jarl stats status
|
|
jarl stats summary --since 7d
|
|
jarl stats top names | clients | nxdomain | servfail | refused | blocked | blocked-clients [--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 | blocklist
|
|
jarl stats server # named's memory, CPU, cache, clients; host; disk space
|
|
```
|
|
|
|
Statistics must be enabled in `jarl.yaml`, and `-o json` exports the data.
|
|
See [stats.md](stats.md).
|
|
|
|
## Blocking
|
|
|
|
```sh
|
|
jarl blocklist list
|
|
jarl blocklist add NAME URL [--view V]... # default: every view that offers recursion
|
|
jarl blocklist update NAME [--url U] [--name N] [--view V | --view auto] [--comment C]
|
|
jarl blocklist disable | enable | delete NAME
|
|
jarl blocklist refresh [NAME...] [--force] # download now (all lists without NAME)
|
|
|
|
jarl block add DOMAIN [--exact] [--view V]... # block it (and the names below it)
|
|
jarl block allow DOMAIN [--exact] [--view V]... # an exception that beats every list
|
|
jarl block list
|
|
jarl block delete DOMAIN [--view V] | --id ID
|
|
jarl block check DOMAIN # blocked? by which rule or list?
|
|
jarl block pause [DURATION] # let everything through for a while (default 10m, at most 24h)
|
|
jarl block pause indefinitely # ... until 'jarl block resume'
|
|
jarl block step list | add NAME | rename STEP NEW | order STEP... | delete STEP
|
|
jarl block group list
|
|
jarl block group add NAME MEMBER... [--step S] [--view V]... # addresses, networks or ACLs
|
|
jarl block group update GROUP [--add M] [--remove M] [--step S] [--name N]
|
|
jarl block group delete GROUP
|
|
jarl blocklist update LIST --step STEP # devices at that step or above get it
|
|
jarl block resume
|
|
```
|
|
|
|
See [blocking.md](blocking.md).
|
|
|
|
## ACME DNS-01
|
|
|
|
```sh
|
|
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](acme.md) for certbot, acme.sh and a dedicated ACME zone.
|
|
|
|
## Scripting tips
|
|
|
|
* `-o id` prints just the IDs of what a `show` or `list` command finds, one
|
|
per line, and `-o field=...` picks fields (their names are those of
|
|
`-o json`; a dot reaches inside, as in `soa.serial`; a field an object
|
|
doesn't have prints as empty). For changes, they apply to the changed
|
|
object:
|
|
|
|
```sh
|
|
ZONE=$(jarl zone show adbraun.lol --view external -o id)
|
|
jarl record list adbraun.lol --name www -t A -o id | xargs -n1 jarl record disable --id
|
|
jarl zone show adbraun.lol -o field=view,soa.serial # external 2026100901
|
|
jarl zone list -o field=id,name,view
|
|
ID=$(jarl record add adbraun.lol www A 192.0.2.10 -o id) # the new record's ID
|
|
```
|
|
* `-q` with `create` commands prints just the ID or token, and with `show`
|
|
and `list` just the IDs. Commands without IDs (status, checks) print
|
|
nothing but errors.
|
|
* `jarl record list` on a terminal shortens data too long for a line
|
|
(`…`); `--wide` shows it in full, and so does output to a pipe or file.
|
|
* Check `$?` against the exit code table.
|
|
* `jarl record delete ... --missing-ok` succeeds when nothing matches, which
|
|
keeps cleanup scripts idempotent. So does `record disable|enable
|
|
--missing-ok`, also when the records are already disabled (enabled). `jarl record set` is idempotent by nature.
|