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>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-08 13:58:50 +00:00
1 parent 6b44b92975
commit 0b2159b1a4
21 files changed
+755 -46

No files matched your search

+1
View File
@@ -315,6 +315,7 @@ Success is `200 {"ok": true, "fqdn": ..., "changeset_id": ..., "zones": [...]}`.
| GET / POST | `/me/tokens` | `{"name": "laptop", "role": "", "grant_strings": [...], "expires_in": "90d"}`. The response's `token` field is the secret, shown once |
| GET | `/tokens` | every token (admin); `?all=1` includes revoked tokens |
| DELETE | `/tokens/{id}` | revoke |
| POST | `/tokens/{id}/rotate` | New secret, same token. Optional body `{"grace": "1h", "expires_in": "90d", "expires_at": "...", "no_expiry": false}`. The response's `token` field is the new secret, shown once; `previous_secret_valid_until` says when the old one stops. A restricted token may rotate itself and tokens no wider than itself |
| POST | `/me/password` | `{"current_password": "...", "password": "..."}` (sessions only) |
User and token changes take effect immediately. They are not changesets,
+3 -1
View File
@@ -272,6 +272,8 @@ 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 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 audit [--user alice] [--action auth.login_failed] [--outcome failure] [--since 2026-10-01] [--limit 100]
@@ -281,7 +283,7 @@ 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).
Grant syntax, roles and token rotation are in [permissions.md](permissions.md).
## Query statistics
+48
View File
@@ -113,6 +113,54 @@ 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: