# Credentials and how they flow restic-manager handles three credential surfaces: 1. **Operator credentials** — the username + password (or OIDC identity) that logs into the UI. 2. **Agent bearer tokens** — issued at enrolment, used by the agent to authenticate its WebSocket to the server. 3. **Repo credentials** — the rest-server / S3 / B2 / SFTP credentials the agent passes to `restic` itself. Each has a different threat model and storage strategy. ## Operator credentials - Local users are stored in `users` with a bcrypt password hash. - Sessions are random tokens minted at login, stored hashed in the `sessions` table, expired after 24h. Cookie is HttpOnly, SameSite=Lax, and Secure (when `RM_COOKIE_SECURE=true`, default). - OIDC users carry `auth_source='oidc'` and an `oidc_subject` pinning their IdP identity. Local password login is rejected for OIDC users. - Disabling a user soft-deletes them via `disabled_at` — pre-existing sessions are invalidated on the next request. ## Agent bearer tokens - Minted at enrolment, hashed at rest with `auth.HashToken`. - The plaintext token only exists in memory at enrolment time and on the agent's filesystem (`/etc/restic-manager/agent.yaml`, mode `0600`, owned by the service user). - Compromise of the server DB leaks the hashes, which is enough to *log in as that agent* until you revoke. Compromise of the agent host leaks the plaintext (via the config file) — same end result. - Rotation: re-enrol the host. Today there's no in-place rotate; the operator deletes the host (which cascades, including revoking the bearer hash) and re-runs the install command. ## Repo credentials This is the credential that ultimately matters for backup integrity. restic-manager keeps two slots per host: - **The everyday credential** (`host_credentials.kind = ''`). Append-only-friendly: this is the one your backup schedule uses. It can write but not delete or forget. - **The admin credential** (`host_credentials.kind = 'admin'`). Has full delete rights. Only pushed to the agent transiently while a `prune` or `forget` job is dispatching, and discarded by the agent after the job ends. ### Encryption flow 1. Operator types the credential into the UI or the install form. 2. Server AEAD-encrypts the cred (`crypto.AEAD.Encrypt`) using the key in `RM_SECRET_KEY_FILE`. The plaintext is dropped from memory. 3. Encrypted blob is stored in `host_credentials.cred_blob`. 4. When the agent connects, the server decrypts the blob and sends the **plaintext** down the WebSocket inside a `config.update` envelope. 5. The agent stores the plaintext in its in-memory secrets store for the lifetime of the process; it's reloaded fresh on every server-side push. 6. When a job runs, the agent merges the credential into the restic environment (`restic.Env.RepoURL` stays bare; the `user:pass@…` form is built only inside `envSlice()` at the moment of `exec.Command`). The merged form is **never logged**. The slog package's structured output gets `restic.RedactURL()` for any URL it has cause to mention. ### Why push plaintext over the wire? The transport itself is the trust boundary: the WebSocket runs inside the same TLS-terminated reverse-proxy connection your browser uses, and the agent has already authenticated with its bearer token. Re-encrypting the payload on top of that would just move the key-management problem somewhere else. If your reverse proxy isn't TLS-terminated, the deployment is already broken — see [Hardening](../security/hardening.md). ## Setup tokens (admin-driven) When an admin creates a new user, the server mints a one-time setup link valid for 1 hour. The hash is stored; the raw token is shown to the admin once. The user opens the link, sets a password, and is dropped into a session. Expired tokens are swept on the alert engine's 60s tick. Same pattern for enrolment tokens: the raw token only exists in memory at mint time, and the install snippet is the operator's only chance to capture it. If you lose it, regenerate via the **Add host** page (NS-02).