🛡️Gatekeeper/ SDKs

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 kindSource on the wireResolves to actorUsed on
bearer (user JWT)Authorization: Bearer <jwt>usertenant routes
apiKeyrequest-body field (/keys/validate, /keys/token)apiKey (tenant-bound, scoped)key validate / token
platformKey (gkp_)Authorization: Bearer <key>platform (service account)platform routes
platformBootstrapbootstrap token in Authorization: Bearer <token>platformBootstrapplatform management only
nonecredential absentanonymouspublic / pre-auth

The actor union (packages/gatekeeper/src/domain/actor.ts) carries only the facts each kind has:

Actor kindCarries
useruserId
apiKeyapiKeyId, tenantId, scopes[]
platformserviceAccountId, 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):

OutcomeActionResult
resolvedproceed with the real actorcontinue
anonymousproceed as anonymousthe policy step decides if anonymous is allowed
invaliddeny with INVALID_CREDENTIAL401
unavailableerror with IDENTITY_BACKEND_UNAVAILABLE503, 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:

StepRoute
Sign upPOST /v1/auth/signup (returns tokens)
Log inPOST /v1/auth/login (returns tokens, or an MFA challenge)
Complete MFAPOST /v1/mfa/verify-challenge (returns tokens)
RefreshPOST /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:

RoutePurpose
POST /v1/keys/validateIntrospect a key; returns its public projection (never the key_hash).
POST /v1/keys/tokenExchange 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:

Verify minted tokens against the public key set:

GET /.well-known/jwks.json

This 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:

  1. A service-account key (gkp_…). The platformKey resolver validates it and yields { kind: 'platform', serviceAccountId, permissions }. A store fault is unavailable (503); anything else is invalid (401).
  2. 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#

See the decision engine internals for the full pipeline, and the error reference for how each outcome maps to an HTTP status.