Files
terdut-server/SERVICE-ACCOUNTS.md
T
Niklas Ye 44b2eb2cc3 Rewrite the README as highlights with screenshots; move the detail into docs/
The README was 1,240 lines of reference material and still described a
SQLite quick start. It is now a short tour (highlights, screenshots of the
web UI, an accurate quick start against Postgres), and each topic has its
own page under docs/ with an index: deployment, configuration, Alertmanager,
incidents, notifications, escalation, dead man's switches, single sign-on,
web UI, API and development. SERVICE-ACCOUNTS.md is rewritten from a
proposal into a reference, and TEAM-LOOKUP.md is gone with the endpoint it
described. The "Upgrading to ..." sections for an unreleased product are
dropped.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
2026-10-09 14:56:13 +02:00

3.0 KiB

Service accounts

A non-human credential for automation (terdut-operator, CI, scripts). It is not a users row: no password, no is_admin, no OIDC identity, so it can never be pulled into login or group sync, and it is never mistaken for a person in an audit trail. The bearer token has the same shape as an API key (SHA-256 hash stored, raw value shown once), prefixed tdsa_.

Scopes

  • instance — acts as owner of every team's configuration (rename, OIDC groups, escalation, dead man's switches, integrations, members, delete) and may create teams. It is not a member of any team, so it reads no incidents or queue. It is never an administrator: user management and /api/admin/settings stay human-only.
  • team — acts as owner of exactly one team, through a single synthetic membership. It may also mint another service account for its own team.

An account has many keys, so rotating is "mint a new key, revoke the old one" without losing the account's identity or history.

Endpoints

  • POST /api/service-accounts {name, scope, team_id} — returns the account and its first key. An instance-scoped account is granted by a human administrator; a team-scoped one by an administrator, that team's owner, or an instance-scoped account.
  • GET /api/service-accounts?name= — look one up by name.
  • POST /api/service-accounts/{id}/keys, DELETE .../keys/{keyID} — mint or revoke a key. An instance-scoped account may manage any team-scoped account's keys, and any account may manage its own.

Seeding the operator's account

TERDUT_OPERATOR_KEY (at least 32 characters) creates the instance-scoped account terdut-operator if missing and replaces its seed key with this value at every start (internal/api/operator_key.go). The deployer generates the key and nothing has to call /api/bootstrap for it; rotating is a restart with a new value. With TERDUT_OPERATOR_MODE on, configuration writes by humans are refused and a service account of either scope passes.

How it is enforced

Every request resolves to one Caller (internal/api/caller.go): a human (session or API key) or a service account.

  • Caller.IsAdmin() is true only for a human administrator. AdminOnly and requireSelfOrAdmin key on it alone; do not widen them — each time a gap came up the fix was a narrower purpose-built capability instead.
  • Caller.IsInstanceServiceAccount() is true only for an instance-scoped account, never for a human. requireTeamOwner and callerOwnsTeam admit it for any team.
  • Caller.Role(teamID)/TeamIDs() are a human's memberships or a team-scoped account's single owner membership; instance scope has none.
  • Caller.AsHuman() is what a handler must call when it needs a real user_id; handlers meant for people answer 403 to a service account instead of writing a zero id.

Where a service account acts on an incident (acknowledge, resolve), the timeline and acknowledged_by record it through parallel *_service_account_id columns, never as a user.