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