# 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).
