Gatekeeper resolves exactly five credential kinds into five actor kinds. The runtime adapter extracts a credential from the transport (header, body, or signature); the decision engine resolves it to an actor and decides allow or deny. Anything unwired resolves as invalid and fails closed.
Credential to actor#
| Credential kind | Source on the wire | Resolves to actor | Used on |
|---|---|---|---|
bearer (user JWT) | Authorization: Bearer <jwt> | user | tenant routes |
apiKey | request-body field (/keys/validate, /keys/token) | apiKey (tenant-bound, scoped) | key validate / token |
platformKey (gkp_) | Authorization: Bearer <key> | platform (service account) | platform routes |
platformBootstrap | bootstrap token in Authorization: Bearer <token> | platformBootstrap | platform management only |
none | credential absent | anonymous | public / pre-auth |
The actor union (packages/gatekeeper/src/domain/actor.ts) carries only the facts each kind has:
| Actor kind | Carries |
|---|---|
user | userId |
apiKey | apiKeyId, tenantId, scopes[] |
platform | serviceAccountId, permissions[] |
platformBootstrap | (no fields - break-glass identity) |
anonymous | (no fields) |
Identity outcomes#
The identity step keeps "no credential" and "wrong credential" distinct (ports/identity.ts):
| Outcome | Action | Result |
|---|---|---|
resolved | proceed with the real actor | continue |
anonymous | proceed as anonymous | the policy step decides if anonymous is allowed |
invalid | deny with INVALID_CREDENTIAL | 401 |
unavailable | error with IDENTITY_BACKEND_UNAVAILABLE | 503, fail closed |
Anonymous passing the identity step is intentional: a route guarded by a policy that requires identity will still reject anonymous later in the authorization step with UNAUTHENTICATED (401), while a genuinely public route needs no special-casing.
User bearer JWT#
Send the user's access token as a bearer token:
Authorization: Bearer <access-token>guard() derives a bearer credential from the header by default. The bearer resolver calls verifyAccessToken; a null payload (bad token or infra fault) is treated as an invalid credential and fails closed. On success the actor is { kind: 'user', userId: payload.sub }.
Get a token from the auth flow:
| Step | Route |
|---|---|
| Sign up | POST /v1/auth/signup (returns tokens) |
| Log in | POST /v1/auth/login (returns tokens, or an MFA challenge) |
| Complete MFA | POST /v1/mfa/verify-challenge (returns tokens) |
| Refresh | POST /v1/auth/refresh (new access token) |
In the SDK, set the bearer once and it rides every request: core.setToken(accessToken) (TS) / core.set_token(access_token) (Python). Public auth routes are called with the bearer omitted.
API key and RS256 token exchange#
An API key (orka_live_…) is its own credential and is passed in the request body, not the Authorization header. Two public, rate-limited routes accept it:
| Route | Purpose |
|---|---|
POST /v1/keys/validate | Introspect a key; returns its public projection (never the key_hash). |
POST /v1/keys/token | Exchange a valid key for a short-lived RS256 JWT. |
The apiKey resolver runs validateApiKey; on success the actor is { kind: 'apiKey', apiKeyId, tenantId, scopes }. A revoked, expired, or unknown key resolves invalid (401).
POST /v1/keys/token mints a signed JWT for service-to-service calls:
- It requires asymmetric signing to be configured; otherwise it returns an error envelope (
token signing not configured). - A scopeless key is rejected with 403 (
api key has no scopes; assign scopes before minting a token) - there is no implicit wildcard. - On success it returns
{ token, tokenType: 'Bearer' }.
Verify minted tokens against the public key set:
GET /.well-known/jwks.jsonThis returns the RS256 JWKS used to verify tokens this control plane mints, or { keys: [] } when asymmetric signing is unconfigured.
Platform service accounts and the bootstrap token#
Platform routes (/v1/platform/*) authenticate a platform credential from the Authorization: Bearer header. Two things can satisfy it:
- A service-account key (
gkp_…). TheplatformKeyresolver validates it and yields{ kind: 'platform', serviceAccountId, permissions }. A store fault isunavailable(503); anything else is invalid (401). - The bootstrap token. Compared against the configured token in constant time; on match it yields
{ kind: 'platformBootstrap' }.
Every platform route requires a platformPermission policy naming the permission it needs (for example service_accounts:write, jobs:read, jobs:write, billing:write).
Fail-closed guarantees#
- A credential kind with no resolver wired resolves as invalid - never as anonymous-allowed.
- A backend fault (
unavailable) becomes a 503 error, never an allow. - A thrown exception in the engine is caught and converted to an internal 503, with the actor reset to anonymous.
- An allow must carry an authenticated actor; an inconsistent allow is downgraded to a 503 error.
See the decision engine internals for the full pipeline, and the error reference for how each outcome maps to an HTTP status.