A template for tokens and users that get certificates for other hosts (jarl cert get): certificates:write, to narrow with names= in the web UI's "Insert a template…", or --role certificates in the CLI. The token form's template filter no longer counts "*" as covering certificates, as the server doesn't. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
347 lines
18 KiB
Markdown
347 lines
18 KiB
Markdown
# 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](#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](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](#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](api.md#authentication)).
|
|
|
|
**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](configuration.md#web-ui-and-api)):
|
|
|
|
| `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 and `jarl status` point this out while an
|
|
enabled `admin` exists next to other administrators.
|
|
|
|
Nothing is lost by disabling it: root on the server always gets back in
|
|
([Recovering access](#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, use the registrar (Porkbun,
|
|
whose key can move the domain), and manage TLS certificates (whose keys
|
|
can impersonate the server; [certificates.md](certificates.md)). The one
|
|
exception is certificates for other hosts, below.
|
|
|
|
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](blocking.md)); `write` also downloads lists now |
|
|
| `certificates` | certificates for other hosts (`jarl cert get`), by the names in them: `write` gets one obtained, `read` only downloads it. Never part of `*` |
|
|
| `*` | all of the above, except `certificates` |
|
|
|
|
`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 dot
|
|
* `views=internal`: view names (`_default` means zones outside any view)
|
|
* `names=_acme-challenge.*`: record owner names, as FQDNs without the final dot
|
|
(for `certificates`: the names in the certificate; `*.adbraun.lol` also
|
|
allows the wildcard certificate `*.adbraun.lol`)
|
|
* `types=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
|
|
certificates:write names=mail.adbraun.lol,smtp.adbraun.lol certificates for a mail server (jarl cert get)
|
|
```
|
|
|
|
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. They also see the zone's
|
|
view in `jarl view list` (`GET /views`): its name, colour and how many
|
|
zones they can see there, marked `limited`, but not its settings
|
|
(`match-clients` and the rest need `views:read`).
|
|
|
|
A zone name that is in several views needs a view (`--view`), unless the
|
|
user can see the zone in only one of them: a token for
|
|
`views=external` finds the external copy without being told.
|
|
|
|
### 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` |
|
|
| `certificates` | `certificates:write`: certificates for other hosts (`jarl cert get`) for any name in Jarl's zones. Narrow it to a host's names, e.g. `certificates:write names=web01.adbraun.lol` ([certificates.md](certificates.md#certificates-for-other-hosts)) |
|
|
|
|
```sh
|
|
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.
|
|
|
|
```sh
|
|
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 create web01 --grant "certificates:write names=web01.adbraun.lol" # jarl cert get on one host
|
|
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-Expires` header,
|
|
and the `jarl` CLI prints a warning on stderr when it sees it, so the
|
|
logs of the script using the token show it;
|
|
* `jarl token list --all --expiring` lists them (`--expiring 60d` looks
|
|
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.
|
|
|
|
```sh
|
|
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 list` and 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 30d` sets
|
|
a new lifetime and `--no-expiry` removes the expiry. A token without an
|
|
expiry keeps having none.
|
|
* **Where the new secret goes**: it is printed once, like a new token.
|
|
`--write FILE` writes it to a file instead (an existing file keeps its
|
|
owner and mode; a new one gets mode 0600). `--save` puts it where the CLI
|
|
got its token from: the configuration file (the selected server's entry)
|
|
or the file named in `--token @FILE`. `--save` refuses before rotating
|
|
when the token came from a literal `--token` or `JARL_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:
|
|
|
|
```sh
|
|
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`) with `socket` as the method. That name isn't a Jarl
|
|
user, so it has no tokens of its own: `sudo jarl token create laptop
|
|
--user admin` makes 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](cli.md#on-the-server-itself)).
|
|
* 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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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)
|
|
```
|