Authentication¶
Two auth surfaces:
- Browser sessions — WebAuthn passkey + cookie. The primary path.
- Scripts / CLI — bearer token. Secondary path for
curl/pluma auth …subcommands.
Both gate /api/* through the same middleware.
Passkey enrollment¶
WebAuthn registration ceremony, server-side state in server/auth.go.
Per-RPID grace¶
The middleware lets a request through unauthenticated when:
require_auth = false(dev override), OR- The request is from a loopback address AND
loopback_auth_bypass = true, OR - No credentials are enrolled for the current origin's RPID yet.
That third condition is the "first pair" grace. Without it the first user on a new origin couldn't reach the API to start the enrollment dance. The grace closes the moment the first credential is registered for that RPID.
Per-origin: enrolling on loopback doesn't gate the tailnet URL, and vice versa. Each origin starts in grace until the first enrollment lands.
RPID collapse on tailnet hosts¶
On *.ts.net hostnames, Pluma sets the WebAuthn RPID to the tailnet parent (e.g. pluma.alice.ts.net → alice.ts.net). One passkey enrolled there covers every tailnet subdomain on that tailnet. The collapse only fires for .ts.net; LAN IPs and other hostnames stay at their full origin.
Sessions¶
Successful /api/auth/login/finish sets a session cookie:
__Host-pluma_sess(HTTPS) orpluma_sess(HTTP loopback)HttpOnly,SameSite=Lax, signed with a per-install session key- 30 days inactivity timeout
The session key lives at <datadir>/session-key (created on first boot; 32 random bytes; mode 0o600). Reset by deleting the file — logs everyone out, all enrolled passkeys are still valid for a fresh login.
HSTS is set on HTTPS responses (max-age=31536000; includeSubDomains).
Bearer tokens¶
pluma auth login performs the WebAuthn ceremony against a running server, then calls POST /api/auth/token to mint a bearer token. The token lands at ~/.config/pluma/cli.token (mode 0o600).
Server-side tokens live in <datadir>/tokens.json (encrypted). Each token carries a label so you can revoke an individual script's access without rotating everything.
Use the token via Authorization: Bearer <token> header OR ?api_token=<token> query param. Order: header wins.
Revoke from Settings → Privacy → Enrolled passkeys (tokens are listed alongside; the UI surface is shared).
Middleware chain (auth-relevant)¶
withTrustedProxies → withLogging → withNoStoreOnUserContent
→ withAuth → withHostAllowlist → withCORS → mux
withAuth runs BEFORE withHostAllowlist because passkey enrollment needs to work from any origin (the allowlist might not include the new device yet). Once the user enrols a passkey, subsequent requests get auth'd; the allowlist still gates whether they reach the API at all from a non-allowed host.
Reset paths¶
| Want to | Do |
|---|---|
| Disable auth entirely (dev) | require_auth = false in config |
| Bypass for the host machine | loopback_auth_bypass = true (default) |
| Force re-enrollment | Delete credentials.json (keeps everything else) |
| Logout everyone | Delete session-key (or call POST /api/auth/logout per session) |
| Lose all bearer tokens | Delete tokens.json |