DocsGovernanceService Accounts

Service Accounts

A service account is a non-human principal owned by a human, for scripts, scheduled jobs, and machine-to-machine integrations. It is a rootcx_system.users row with kind = 'service': a random UUID, the email sa+{slug}@localhost, and no password or SSO identity.

A service account is governed by exactly the same RBAC, enforcement, delegation, and worker isolation as every other principal. The only net-new surface is its lifecycle, its credentials, a client-credentials token endpoint, and act-as grants.

Managing service accounts requires the admin:service_accounts.manage permission.


Create

POST /api/v1/service-accounts
{ "slug": "nightly-sync", "displayName": "Nightly Sync Job" }

The slug matches [a-z0-9_-], max 48 characters, and becomes the account's email (sa+nightly-sync@localhost). Returns 201 with the account id.

A service account is born with no permissions (deny-by-default). Grant it a least-privilege role through the standard roles API before it can do anything.


Credentials

A service account authenticates with one or more API keys.

POST /api/v1/service-accounts/{id}/credentials
{ "name": "ci-pipeline", "expiresInDays": 90 }

expiresInDays defaults to 90 and is clamped to 1-365. The response returns the key once:

{
  "id": "9c1f...",
  "key": "rcs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "prefix": "rcs_xxxxxxxx",
  "expiresAt": "2026-09-04T10:00:00+00:00",
  "note": "store this key now; it is shown only once"
}

The key is a 256-bit secret prefixed with rcs_. Only its SHA-256 hash is stored; the plaintext is never persisted and never shown again. An indexed prefix (the first 12 characters) makes lookup fast without weakening the secret.

Revoke a key at any time:

DELETE /api/v1/service-accounts/{id}/credentials/{credId}

Getting a token

Exchange a credential for a short-lived access token using the OAuth 2.0 client-credentials grant (RFC 6749 §4.4):

curl -X POST http://localhost:9100/api/v1/auth/token \
  -d grant_type=client_credentials \
  -d client_id=<service-account-id> \
  -d client_secret=rcs_...

Returns a Bearer token:

{ "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 900 }

The exchange fails if the service account is disabled, has no owner of record, or the credential is wrong, revoked, or expired. Use the token like any other: Authorization: Bearer <access_token>.


Act-as: running work as a service account

A human can run an automation as a service account, so the work is owned and attributed to that account instead of the person. This is ownership, not impersonation. It passes a strict, double-locked gate, with exactly 1 way in and no bypass. See Delegation for the principle.

1. A standing act-as delegation must exist.

POST /api/v1/service-accounts/{id}/act-as
{ "userId": "<human-uuid>" }

This grants the human the right to act as the service account. Revoke it with DELETE on the same path.

2. Anti-escalation: the service account's permissions must be a subset of the human's.

SERVICE ACCOUNT app:crm:contacts.read app:crm:contacts.create YOU (ALICE) app:crm:* ALLOWED SERVICE ACCOUNT app:crm:contacts.read admin:secrets.manage YOU (ALICE) app:crm:* BLOCKED

Every time the human invokes act-as, the Core checks that the service account holds nothing the human does not already hold. You can never gain authority by routing through a service account, only narrow or match your own.

A human acting as itself is always allowed and skips both checks.


Lifecycle

Action Endpoint Effect
Disable POST /api/v1/service-accounts/{id}/disable Cuts access immediately; tokens are refused
Enable POST /api/v1/service-accounts/{id}/enable Restores access
Transfer ownership POST /api/v1/service-accounts/{id}/transfer-ownership Reassigns the owner; the new owner must be a human
Delete DELETE /api/v1/service-accounts/{id} Revokes standing delegations, cascades credentials, removes the principal

Disabling is immediate because the check runs live against the database on every request. There is no token to wait out.


API endpoints summary

Method Path Description
GET /api/v1/service-accounts List service accounts
POST /api/v1/service-accounts Create a service account
DELETE /api/v1/service-accounts/{id} Delete a service account
POST /api/v1/service-accounts/{id}/disable Disable
POST /api/v1/service-accounts/{id}/enable Enable
POST /api/v1/service-accounts/{id}/credentials Mint a credential (returned once)
DELETE /api/v1/service-accounts/{id}/credentials/{credId} Revoke a credential
POST /api/v1/service-accounts/{id}/transfer-ownership Transfer ownership to a human
POST /api/v1/service-accounts/{id}/act-as Grant a human the right to act as the account
DELETE /api/v1/service-accounts/{id}/act-as Revoke an act-as grant
POST /api/v1/auth/token Client-credentials token exchange

All management endpoints require admin:service_accounts.manage.