Add two-factor login (TOTP) with recovery codes
Users can set up an authenticator app from their account page; login then asks for a code after the password, with one-time recovery codes as a fallback. Sensitive actions (tokens, user changes, 2FA resets) need a code from the last 5 minutes. totp.require (none/admins/all) forces setup, and admins can reset a user's 2FA from the UI, the jarl CLI or jarld on the server. The dashboard and jarl status flag users missing 2FA and a shared "admin" account still in use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b22ffee1bb
commit
8f9cdd0a17
35 files changed
+1965
-43
No files matched your search
+90
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
| 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. |
|
||||
| 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). |
|
||||
@@ -24,6 +24,94 @@ 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`)
|
||||
@@ -237,4 +325,5 @@ 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)
|
||||
```
|
||||
Reference in new issue
Block a user