Authentication
Bearer tokens that authenticate a service account, how a request finds its workspace, and the distinct error code behind every kind of refusal.
The wire format
Authorization: Bearer krm_live_a1b2c3d4<32-byte url-safe secret>A token is krm_live_, then an 8-character prefix, then the secret. The
prefix is the visible identifier — it is what the token list in Krrim shows you,
and what appears in audit rows. Only the prefix is stored readably; the rest is
stored as a SHA-256 hash.
Tokens are hashed, not encrypted. We never need the original value again — only to verify one you present — so a database leak does not leak working tokens. This is the opposite of a webhook signing secret, which we do have to present on every delivery and therefore must store reversibly.
The fixed krm_live_ marker exists so a token pasted into a support thread is
recognisable, and so GitHub secret scanning can report a leak. If a token of
yours is found in a public repository, Krrim is notified and the token is
revoked automatically.
A token is a service account
Every authorization check in Krrim takes a user. Rather than invent a parallel
permission system for machines, a token's principal is a service account — a
real User row with is_service_account = true and its own membership at a role
an admin picked.
That means:
- The automation's permissions are chosen deliberately, not inherited from its creator and silently widened when that person is promoted.
- Offboarding a human never breaks a production integration.
- Audit rows name the bot, not a person who did not do it.
A service account cannot sign in (no usable password, and the login views
reject the flag explicitly), and it can be neither owner nor guest.
Effective permission
what a token can do = the service account's role
∩ the token's scopes
∩ the token's project restrictionAll three narrow. Adding a scope never grants something the role does not already allow, and restricting a token to two projects never widens it to a third.
Requests route by Host
Krrim is multi-tenant with a Postgres schema per workspace, and the Host header
is how a request finds its schema. So there is no shared API hostname — you call
your own:
https://<your-workspace>.krrim.com/api/v1/...A token presented to a host that routes elsewhere is refused with
WRONG_TENANT_HOST, rather than being allowed through to fail confusingly deep
inside a view.
Every refusal has its own code
401 Unauthorized with no discriminator is the most expensive thing an API can
do to someone debugging at 2am — expired, revoked and wrong-workspace each need
a different fix, and only the caller can tell which they hit. So each gets its
own stable error.code:
| Code | Status | What to do |
|---|---|---|
TOKEN_INVALID | 401 | The value is malformed or unknown. Check for truncation. |
TOKEN_EXPIRED | 401 | Past expires_at. Mint a new one; expiry is mandatory. |
TOKEN_REVOKED | 401 | Revoked in the workspace. Revocation is one-way — mint a new one. |
WRONG_TENANT_HOST | 401 | Right token, wrong host. Call your own workspace subdomain. |
INSUFFICIENT_SCOPE | 403 | Published endpoint, missing scope. Mint a token with the scope. |
INSUFFICIENT_ROLE | 403 | Scope is present but the service account's role is too low. |
FORBIDDEN | 403 | The endpoint is not published to tokens at all. |
The split between INSUFFICIENT_SCOPE and INSUFFICIENT_ROLE matters because
they are fixed in different places: one is a new token, the other is a
membership change.
Transport rules
- HTTPS only. The auth layer rejects a token carried over plain HTTP rather than relying on a redirect, which would have already put the secret on the wire.
- Header only. Not a query parameter, not a cookie.
- No CSRF token. A bearer header is not an ambient credential, so there is nothing to forge cross-site.
Revocation
Revoking is immediate and irreversible. There is no un-revoke — mint a new token. The prefix lookup is cached briefly for performance and invalidated the moment a token is revoked, so revocation takes effect on the next request rather than at the end of a cache window.
To stop every token at once without destroying any of them, disconnect the Public API integration. That is the kill-switch; revocation is not the right tool for "turn it off while we investigate".
Auditing
Every write through a token writes an audit row carrying the service account as
actor, plus the source IP, user agent and request id. last_used_at is recorded
too, at most once per minute per token — often enough to spot a token nobody
uses any more, rarely enough that a read-only GET does not become a database
write.