Authentication
Every governed cell route accepts identity from exactly one place: a
verified Authorization: Bearer <jwt>.
Authorization: Bearer <jwt>There is no trusted identity header and no header-trust switch — X-Actor-Roles
and X-OSL-Subject are always ignored, even if a proxy sends them. A missing
token denies by default; a present-but-invalid token (bad signature, expired,
wrong shape) is a hard 401 E3001. The engine never downgrades a broken token
to “anonymous”.
The token: ActorToken v2
Section titled “The token: ActorToken v2”Cell bearer tokens are ActorToken v2 JWTs:
| Field | Requirement |
|---|---|
Header typ |
must be opendome-actor+jwt |
Header alg |
HS256 (default) or RS256 |
version |
2 |
tenant_id |
the tenant this token is scoped to |
chain[] |
non-empty delegation chain, root → leaf |
sub |
must equal chain[-1] (the acting subject) |
roles[] |
control permissions (e.g. TENANT_ADMIN) — admin operations only |
acl_roles[] |
data-read permissions — the only input for Lance ACLs |
iat / exp / jti |
required; max TTL 300 s, validated with 30 s clock skew |
iss / aud |
required when the verifier is configured with them (chart default issuer: opendome) |
The delegation chain
Section titled “The delegation chain”Access is evaluated over a chain of subjects (root → leaf), not a single identity — see Access & capabilities. The chain is inside the signed payload, so a caller cannot shorten or forge it without breaking the signature:
{ "typ": "opendome-actor+jwt", "version": 2, "tenant_id": "demo", "iss": "opendome", "chain": ["ana@example.com", "agent://copilot-finance"], "sub": "agent://copilot-finance", "roles": [], "acl_roles": ["analyst"], "iat": 1750000000, "exp": 1750000300, "jti": "01HV…"}The signing Secret (standalone default: HS256)
Section titled “The signing Secret (standalone default: HS256)”The tenant-semantic-api chart ships with auth.jwt.enabled: true: it creates
(or reuses on upgrade) an HS256 signing Secret named tenant-semantic-api-jwt
in the tenant namespace, and the engine verifies bearer tokens against it. The
other cell charts verify against the same Secret by default, so one Secret
per tenant covers the whole cell.
For GitOps / render-only pipelines, set auth.jwt.existingSecret to a Secret
you manage — a pure render cannot look up the live Secret and would otherwise
rotate it on every render.
Minting a token: scripts/osl-token.sh
Section titled “Minting a token: scripts/osl-token.sh”scripts/osl-token.sh (in the platform repository)
reads the chart-generated Secret via kubectl and signs an HS256 ActorToken v2
with the Python 3 stdlib only. Defaults: sub=local-admin,
acl_roles=["analyst"], TTL 300 s.
TOKEN=$(scripts/osl-token.sh demo)
curl -fsS "http://$HOST/osl/query" \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"osql":"SELECT customer_id, name FROM osl.entities.customer LIMIT 5"}'Useful flags:
scripts/osl-token.sh demo --acl-roles analyst,finance # data-read roles (Lance ACL input)scripts/osl-token.sh demo --control-roles TENANT_ADMIN # control roles (admin routes)scripts/osl-token.sh demo --sub agent-1 --chain root,agent-1 # delegation chain (sub = leaf)scripts/osl-token.sh demo --ttl 60 --audience osl # TTL ≤ 300 s; optional audienceBYO OIDC — the production upgrade path Apache-2.0
Section titled “BYO OIDC — the production upgrade path ”A production self-hoster can replace the chart-minted HS256 Secret with its own issuer — this is part of the open-source distribution, not an Enterprise gate:
- Set
auth.jwt.algorithm: RS256and pointauth.jwt.existingSecretat a Secret carrying your issuer’s public verifying key. The verifier never needs the private key. - Set
auth.jwt.issuer/auth.jwt.audience— once configured they become required claims. - Your issuer must emit the ActorToken v2 shape above (the
opendome-actor+jwtheadertypand the required claims, typically via protocol/claim mappers). Tokens without that shape are rejected — the contract is deliberately hard-cut.
The repository’s docs/auth.md
walks through configuring a local Keycloak as a test OIDC issuer.
Rate limits planned
Section titled “Rate limits ”The reference engine does not yet enforce in-process rate limits. The intended
defaults (per subject) are 60 rps for POST /osl/query and 5 rps for the cached
GET /osl/schema; exceeding them will return 429 with Retry-After.
Every request — 200 or not — produces an audit entry with request_id,
subject, action, the object FQNs touched, the decision and a timestamp.