Files
jarl/docs/permissions.md
T
abrown38andClaude Opus 5.5 0b2159b1a4 Add token rotation
jarl token rotate, a Rotate button in the web UI and
POST /api/v1/tokens/{id}/rotate give a token a new secret while it keeps
its ID, name, user and grants. The old secret stops at once or after a
grace period of up to 7 days, during which jarld logs who still uses it.
A token that expires gets its lifetime again. --save writes the new
secret where the CLI got the old one (its configuration or --token @FILE),
--write FILE to any file, and --current picks the token the CLI runs
with, so a restricted token can renew itself. A restricted token can't
rotate a wider token of its user.

Also fixes token create --save for named servers, and future times
(token expiry) shown as "just now" in the web UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:58:50 +00:00

174 lines
8.2 KiB
Markdown

# Users, tokens and permissions
## Ways to authenticate
| Who | How | Notes |
|-----|-----|-------|
| People in the web UI | Username + password → 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 | `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.
## 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
and run `resync`.
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) 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) |
| `*` | 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 dot
* `views=internal`: view names (`_default` means zones outside any view)
* `names=_acme-challenge.*`: record owner names, as FQDNs without the final dot
* `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
```
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` |
| `operator` | `*:write` (everything except user management) |
| `acme` | `records:write names=_acme-challenge.* types=TXT` |
```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 list --all
jarl token revoke 12
```
In the web UI: Account → API tokens (your own) and Users → *user* →
Tokens (admin).
### 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.
## Recovering access
On the server, 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
```