jarld downloads blocklists (Adblock, hosts or plain domains), checks them daily and reloads named only when a list changed; your own block and allow rules take precedence, and the most specific rule wins. Lists and rules apply per view (default: every view that offers recursion). Web UI Blocking page with name checks, jarl blocklist and jarl block, the /blocking API, blocked counts in the statistics. Database schema 6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
16 KiB
Users, tokens and permissions
Ways to authenticate
| Who | How | Notes |
|---|---|---|
| People in the web UI | Username + password (+ a code from an authenticator app, with two-factor login) → session cookie | argon2id password hashes. The session cookie is HttpOnly and SameSite=Strict and lasts 12h (2h idle). Every change also needs the session's CSRF token, which the UI sends in X-CSRF-Token. Changing a password logs out the user's other sessions. |
| CLI, scripts, certbot | API token: Authorization: Bearer jarl_... |
Shown once at creation, stored hashed. Optional expiry. Revocable. Last use time and IP are recorded. |
lego / Traefik (httpreq) |
HTTP Basic: the token's user and the API token | Only on /api/v1/acme/httpreq/..., only when acme.httpreq.enabled is set, and by default only over HTTPS (acme.md). The token's grants apply as usual. |
| Root on the server, with the CLI | jarld's local socket (/run/jarl/jarl.sock): no token |
sudo jarl ... with no server configured. The kernel tells jarld which system user is connecting; root and the jarl user are administrators, anyone else must send a token. Audited as the system user (e.g. root) via socket. See The local socket. |
| 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.
A user without a password (--no-password) is a service account. It
can't log in to the web UI and is used only through its tokens, which suits
certbot, CI jobs and backups.
Two-factor login
Users can protect their web login with a second factor: a 6-digit code from an authenticator app on their phone (TOTP: Aegis, Google Authenticator, 1Password, Bitwarden and the like). A stolen password alone then gets nobody in.
Setting it up (Account → Two-factor login): enter your password, scan the QR code (or type the key shown under it), and enter the code the app shows. Only then is it on, so a bad scan can't lock you out. Jarl then shows 10 recovery codes, once: save them in a password manager or print them. Each logs you in once instead of a code, for when the phone is lost. Turning it on logs out your other sessions.
Logging in: username and password first; the second screen asks for the code (or, under "Lost your phone?", a recovery code). The password step alone gives no session: it is good for 5 minutes and 5 tries. Codes from one 30-second step either side of now are accepted, for clocks that are a little off, and each code works only once. Wrong codes count against the same rate limit as wrong passwords.
Actions that ask for a code again. Some things would let a stolen
session or password outlive the second factor, so with two-factor login
they need a code from the last 5 minutes (logging in counts): creating
or rotating API tokens, creating, changing or deleting users, and
resetting someone's two-factor login. The web UI asks for a code when
needed; API clients send it in the X-Jarl-TOTP header
(api.md).
What it doesn't cover: API tokens and the local socket. A token is already its own long random secret, and scripts can't answer a code prompt, which is why making one asks for a code. Root on the server can read the database anyway, so a code would add nothing there.
Making it compulsory: totp.require in jarl.yaml
(configuration.md):
totp.require |
Who must use it |
|---|---|
none (default) |
nobody; each user decides |
admins |
administrators |
all |
every user with a password (service accounts can't log in anyway) |
A user who must use it and hasn't set it up yet can log in with the
password, but then only sees "Set up two-factor login" until they do. They
can't turn it off while it is required (moving to a new phone is "Move to
a new phone" under Account). The setting lives in jarl.yaml rather than in
the web UI on purpose: someone who steals an administrator's login can't
switch it off. The dashboard and jarl status list users who must use it
but haven't set it up.
Lost phone:
- With a recovery code: log in with it, then Account → Move to a new phone. New recovery codes replace the old ones (Account → New recovery codes; Jarl warns when 3 or fewer are left).
- Without: an administrator resets it (Users → Reset 2FA, or
jarl user totp reset NAME). That turns it off, deletes the recovery codes and logs the user out; they log in with the password and set it up again. - No administrator can log in: on the server,
sudo -u jarl jarld user totp reset NAME(works with jarld stopped too).
Every setup, change, failed code, recovery code use and reset is in the
audit log (user.totp_*, auth.totp_failed).
The secret is stored in the database, like TSIG secrets (it can't be
hashed: the server needs it to work out the codes), so it is as safe as
the database file and its backups (mode 0600, root and jarl only).
Recovery codes are stored hashed.
One account per person
jarld init creates the first administrator, admin unless you name it
(-u NAME). If several people use that one account, nobody can tell from
the audit log who did what, and it can't have two-factor login per person.
So:
- Just you: rename it to your name (
jarl user update admin --rename adam, or Users → Edit) and turn on two-factor login. - Several people: give each their own administrator account with their
own two-factor login, then disable
admin(jarl user update admin --disabled). The dashboard andjarl statuspoint this out while an enabledadminexists next to other administrators.
Nothing is lost by disabling it: root on the server always gets back in (Recovering access), and Jarl refuses to disable, demote or delete the last enabled administrator.
Grants
A user's permissions are a list of grants. Administrators (admin: true)
may do everything, and only they can manage users, see everyone's tokens,
run resync, wipe a zone's DNSSEC keys, and use the registrar (Porkbun,
whose key can move the domain).
A grant is written as:
RESOURCE:ACCESS [zones=PATTERNS] [views=PATTERNS] [names=PATTERNS] [types=TYPES]
| Resource | Covers |
|---|---|
zones |
zone definitions (create, delete, SOA, TTL, view, DNSSEC signing and the DS check) and zone options |
records |
records in primary zones |
views |
views and view options |
acls |
ACLs |
tsig |
TSIG keys (write is needed to see a secret) |
options |
server options, top-level statements, the defaults for new zones, jarl check |
audit |
everyone's audit events and changesets (without it, users see only their own) |
stats |
query statistics, including client addresses and the names they look up (read only) |
blocking |
blocklists and block/allow rules (blocking.md); write also downloads lists now |
* |
all of the above |
ACCESS is read or write (write includes read). Filters narrow a grant.
Each takes comma separated patterns where * and ? are wildcards, and a
missing filter matches everything:
zones=adbraun.lol,*.adbraun.lol: zone names, without the final dotviews=internal: view names (_defaultmeans zones outside any view)names=_acme-challenge.*: record owner names, as FQDNs without the final dottypes=TXT,CNAME: record types
Examples:
*:read read everything
records:write zones=lab.adbraun.lol edit records in one zone
zones:write zones=*.lab.adbraun.lol create/delete zones below lab
records:write zones=adbraun.lol views=internal only the internal copy of a split zone
records:write names=_acme-challenge.* types=TXT ACME challenges anywhere
records:write zones=acme.adbraun.lol types=TXT only TXT records in a dedicated ACME zone
A user who may see some records of a zone may also see that the zone exists (its name and type), but not its other records.
Roles
Roles are ready-made grant lists. Choosing a role copies its grants, which you can then edit.
| Role | Grants |
|---|---|
readonly |
*:read (includes statistics) |
dns-editor |
zones:write, records:write, views:read, acls:read, options:read, blocking:read |
operator |
*:write (everything except user management) |
acme |
records:write names=_acme-challenge.* types=TXT |
jarl user create alice --role dns-editor
jarl user update alice --grant "records:write zones=adbraun.lol" --grant "zones:read" # replaces all grants
jarl user update alice --role readonly
Tokens
Every token belongs to a user, and every token request acts as that user. The audit log shows the user and the token name.
- A token without grants has all of its user's permissions, and they follow the user: if the user loses a permission, so does the token.
- A token with grants is restricted to them. Effective permission is user grants ∩ token grants: a token can never do more than its user. Admin rights don't pass through a restricted token.
- A restricted token can create further tokens, but only ones no wider than itself.
- Disabling or deleting a user stops all their tokens at once.
jarl token create laptop # full rights of the user, no expiry
jarl token create ci --grant "records:write zones=lab.adbraun.lol" --expires 90d
jarl token create certbot --user certbot --role acme # admin: a token for another user
jarl token list --all
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-Expiresheader, and thejarlCLI prints a warning on stderr when it sees it, so the logs of the script using the token show it; jarl token list --all --expiringlists them (--expiring 60dlooks 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 its ID, name, user, grants and audit history, so there is nothing to set up again. Only the programs that use the token need the new secret. Use it on a schedule, when someone who knew the token leaves, or when it may have leaked. Revoking is for a token that is no longer needed.
jarl token rotate 12 # by ID (see 'jarl token list')
jarl token rotate laptop --save # by name, and update the CLI's configuration
jarl token rotate deploy --user ci --grace 1h # another user's token (admin), old secret works 1 more hour
jarl token rotate --current --save # the token this CLI runs with
-
The old secret stops working at once. With
--grace(at most 7d) it keeps working for that long, so clients can be switched over without an outage. While it does, jarld logs a warning each time the old secret is used (at most once a minute per token and address), with the client's IP, so you can find what still needs updating:journalctl -u jarl | grep "old secret".jarl token listand the web UI show when the old secret stops. Rotating again ends the previous grace period. If the token may have leaked, rotate without--grace. -
Expiry: a token that expires gets its lifetime again, counted from now, so a 90-day token is good for another 90 days.
--expires 30dsets a new lifetime and--no-expiryremoves the expiry. A token without an expiry keeps having none. -
Where the new secret goes: it is printed once, like a new token.
--write FILEwrites it to a file instead (an existing file keeps its owner and mode; a new one gets mode 0600).--saveputs it where the CLI got its token from: the configuration file (the selected server's entry) or the file named in--token @FILE.--saverefuses before rotating when the token came from a literal--tokenorJARL_TOKEN. -
Who may rotate: administrators any token; users their own tokens. A restricted token may rotate itself and tokens no wider than itself, but not a wider token of its user, since it would learn that token's secret. So automation can renew its own token, for example a monthly timer:
jarl --token @/etc/jarl/certbot.token token rotate --current --save -
Every rotation is in the audit log (
token.rotate), with when the old secret stops and the new expiry.
In the web UI, the Rotate button next to each token asks how long the old secret should keep working and how long the token should last, then shows the new secret once.
The local socket
jarld also listens on a unix socket, /run/jarl/jarl.sock (socket in
jarl.yaml; systemd creates /run/jarl). It serves the same API, but a
request on it without a token is trusted by who sent it: the kernel
reports the connecting process's user (SO_PEERCRED), and
| Connecting user | Without a token | With a token |
|---|---|---|
| root | administrator | the token's permissions |
jarl (jarld's own user) |
administrator | the token's permissions |
| anyone else | refused (401) | the token's permissions |
This gives root nothing it doesn't already have: root can read Jarl's
database, run jarld user token and edit named.conf directly. What it adds
is convenience and a way back in: on the server, sudo jarl status and
every other command work with nothing configured and no token stored in a
file, even after every token has been revoked and every password
forgotten.
- The socket file is mode 0600 in a 0750 directory owned by
jarl, so other users can't even connect; the user check doesn't depend on that. - Changes made this way are in the history and audit log under the system
user's name (
root) withsocketas the method. That name isn't a Jarl user, so it has no tokens of its own:sudo jarl token create laptop --user adminmakes one for a real user. - The CLI uses the socket only when no server is configured at all, or
with
-s local, so a configured server is never silently swapped for the local one (cli.md). - Turn it off with
socket: ""in jarl.yaml.
Recovering access
Revoking a token never locks you out: tokens and web sessions are separate. Revoking the token your CLI uses stops only the programs that use it; you stay logged in to the web UI with your password, with all your rights, and can make a new token under Account → API tokens. Jarl also refuses to delete, disable or demote the last administrator.
On the server, root can always use the CLI through the local socket, without a token:
sudo jarl whoami
sudo jarl token create laptop --user admin # a new token for admin
sudo jarl user update admin --password # a new password
The jarld subcommands work even when jarld isn't running. As root or the
jarl user:
sudo -u jarl jarld user list
sudo -u jarl jarld user passwd admin # set a new password and re-enable the account
sudo -u jarl jarld user add --admin rescue # a new administrator
sudo -u jarl jarld user token admin cli # a new API token for a user
sudo -u jarl jarld user totp reset admin # turn off two-factor login (a lost phone)