Authorization
PASETO tokens, the four-tier capability stack, and how CortexDB evaluates authorization.
CortexDB authorizes every request through a four-tier capability stack and identifies callers with PASETO v4 public tokens. Authorization is enforced at the storage layer, not in application code.
The four-tier stack
Decisions cascade from outer to inner. An allow at an inner tier can override an outer allow (be more specific), but an outer deny is final.
1. Deployment policy ← preset-defined floor (cannot be overridden by tenant/scope)
2. Tenant policy ← per-tenant defaults
3. Scope policy ← per-scope ACLs and members
4. Actor policy ← per-actor overridesDeployment presets
Set with CORTEX_DEPLOYMENT_PRESET. These four values are the complete set.
| Preset | Notes |
|---|---|
dev_local | Default. Experimentals enabled. Local development only. (Before v0.9.10 it also accepted unsigned actor-only requests; since v0.9.10 a keyed server rejects them with 401.) |
on_prem_enterprise | Most permissive production preset; cross-workspace GDPR erasure allowed by default. |
cloud_shared_saas | Shared-cloud preset; experimentals gated; cross-workspace GDPR denied by default. |
cloud_private | Single-tenant cloud preset; experimentals gated. |
Authenticating an agent
Every request in a secured deployment carries:
Authorization: Bearer <PASETO v4 public token>
X-Cortex-Actor: user:aliceSelf-hosted auth modes
With CORTEX_API_KEY set, the key value itself is a working Bearer credential (constant-time compared),
alongside PASETO/JWT bearers. To run unauthenticated for local dev, start with
CORTEX_INSECURE_NO_AUTH=1 — requests are served without a token (served as user:local, or as whatever X-Cortex-Actor names; it does not restrict the bind, so publish the port on loopback only, -p 127.0.0.1:3141:3141). With
neither on a network-exposed bind, the server auto-generates a key and requires it. There is no
/v1/auth/login.
On a network, run v0.9.10 or later
Run v0.9.10 or later on any network-reachable server: from v0.9.10 every preset requires the key, and
a request carrying only an X-Cortex-Actor header gets 401 MISSING_TOKEN. If you must run an older
image on a network, set CORTEX_DEPLOYMENT_PRESET=on_prem_enterprise (or a cloud_* preset).
Pick your auth path
| You want to… | Use | Get a token by |
|---|---|---|
| Try the API in 60 s (prototype, AI self-install) | Anonymous sign-up | POST /v1/auth/signup {} → token + actor + scope. Self-hosted, the in-binary minter is off by default on a keyed server (503 NOT_CONFIGURED) — enable with CORTEX_V1_MINTER_ENABLE=1; under CORTEX_INSECURE_NO_AUTH=1 signup is served. Standard on the managed cloud. |
| Run a service account | Server-side mint | Token with auth.mint, then POST /v1/auth/tokens with subject/ttl/scopes. In-binary minter is dev-only. |
| Production with permanent identities | Your IdP issues PASETO | Register your IdP's signing key (IssuerRegistry); CortexDB verifies. |
| Dashboard / MCP server | cx_live_… API key from the dashboard | A separate credential format the BFF exchanges for PASETO server-side. |
Token claims
{
"iss": "https://idp.acme.com/realms/main",
"sub": "user:alice",
"aud": "cortexdb:tenant:acme",
"exp": 1763212929,
"jti": "j_01HX...",
"deployment": "cloud_shared_saas",
"caps": ["scope.read.local", "scope.write"],
"scopes": ["scope.read:org:acme/dept:eng/*", "scope.write:org:acme/dept:eng/user:alice"]
}X-Cortex-Actor must match sub. A token can only narrow what policy already allows — it cannot grant
a capability the deployment denies.
capsnarrows the token to the listed capabilities (empty inherits the deployment and tenant defaults).scopesconfines capabilities to scope subtrees. Each entry is<capability>:<scope-path>[/*]: an exact capability or a prefix (scope.readcoversscope.read.local,scope.read.holistic, …), then the subtree it is confined to. A capability no entry names stays unconfined. A bare path such as"org:acme/dept:eng"is refused at mint with422 INVALID_BODY("expected<capability>:<scope-path>[/*]").- Neither claim makes the caller a member of a scope. Reading or writing a registered scope also
needs a seat on its scope roster; a freshly minted agent that holds the
right capabilities but no seat is refused with
403 POLICY_DENIED.
Denials
A 403 cites the tier + capability + reason:
{
"error_code": "POLICY_DENIED",
"message": "forget.gdpr.cross_workspace denied",
"details": {
"capability": "forget.gdpr.cross_workspace",
"decided_by_tier": "deployment",
"reason": "preset cloud_shared_saas denies"
},
"retriable": false
}Introspect the allow/deny surface with GET /v1/policy/effective?actor=…&scope=…. Note: in v0.9.9, v0.9.13 and v0.10.1 that
endpoint returns allowed/denied as flat capability-name arrays (plus deployment_preset,
rate_limits) — the per-entry tier/reason shown in the denial body is not (yet) returned there. See the
Policy API reference.
Common capabilities
| Capability | Gates |
|---|---|
scope.read.local / .holistic / .descend | read at scope / traverse ancestors / traverse descendants |
scope.write / .elevated / .on_behalf_of / .about_other | write / write above natural scope / set observed_actor / set subject |
forget.cascade.derived_only / .redact_events | selective forget / blank event payloads |
forget.gdpr / .cross_workspace | true event deletion via /v1/erasures / propagate into co-owned workspaces |
llm.invoke | call /v1/answer |
diagnostics.read | request diagnostics ≠ "none" |
understanding.synthesize | trigger /v1/understanding/synthesize |
FAQ
Can an inner policy override an outer one? An inner allow can be more specific; an outer deny is always final.
Does CortexDB use flat tenant identifiers? No — hierarchical scopes, so policy inherits down the hierarchy.