Authentication
Which credential to send, what it can reach, and how requests are scoped.
Every /sandbox/* route (except health) requires a recognised credential. No
anonymous data.
Issuing credentials is a separate privilege: Provisioning credentials.
export SANDBOX_GATEWAY_URL="${SANDBOX_GATEWAY_URL:-http://localhost:8780}"Which credential you have
| Credential | Header | Scope | Issued to |
|---|---|---|---|
| Platform API key | X-Api-Key or Authorization: Bearer … | Any project (via tenancy headers) | First-party callers |
| Keycloak user token | Authorization: Bearer … | Assigned tenant/project roles | People signed in through CTP or another approved application |
| Keycloak service token | Authorization: Bearer … | Service-account role groups | Approved service-to-service callers |
| Admin key | X-Sandbox-Admin-Key | Compute-quota break-glass only | Sandbox operators |
They are not interchangeable. Prefer Keycloak for CTP/canvas. Keep platform API keys internal — they may act for any project when tenancy headers are set.
# Platform API key
curl -H "X-Api-Key: $SANDBOX_API_KEY" \
-H "X-Sandbox-Tenant-Id: acme" \
-H "X-Sandbox-Project-Id: proj1" \
"$SANDBOX_GATEWAY_URL/sandbox/v1/catalog"Choose your integration path
| Caller | How it authenticates | What must be configured |
|---|---|---|
| CTP user | Sign in to CTP; CTP obtains a Keycloak user token | Sandbox role plus structural tenant/project group |
| CTP backend | Existing ctp-app-{env} client credentials | Service-account role/group and Sandbox audience |
| Another Keycloak application or service | Its existing Keycloak client; do not create a sandbox-{consumer}-{env} client | Approved azp, Sandbox audience, role, and structural group |
| No Keycloak integration | Internal platform API key + tenancy headers | Operator-issued API key (not LLM BYOK) |
Sandbox migration to Keycloak does not require every consumer to implement a Keycloak login immediately. Platform API keys remain valid where the target environment enables them. They authenticate normal Catalog, Harbor, Agent, CodeEdit, and Sandboxes calls; they do not receive Keycloak's fine-grained role decisions.
Using Keycloak through CTP
sandbox-dev, sandbox-preprod, and sandbox-prod are resource-server audiences,
not login applications. Their interactive login, password grant, and direct access
grant are disabled. A person does not log in to a Sandbox client and must never be
given a client secret.
A CTP user signs in through the existing CTP application. The resulting access token can call Sandbox directly, or CTP can forward it, when all of these are true:
audcontains the matchingsandbox-{env}audience;azpis an approved caller such asctp-app-{env};resource_access.sandbox-{env}.rolescontains the Sandbox role; andgroupscontains the full structural tenant/project role path.
curl -sS \
-H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" \
-H "X-Sandbox-Tenant-Id: acme" \
-H "X-Sandbox-Project-Id: proj1" \
"$SANDBOX_GATEWAY_URL/sandbox/v1/catalog"Headers select within the token's structural authority. Changing proj1 to a
project absent from the token's groups returns 403; headers never grant access.
CTP backend jobs use the existing ctp-app-{env} client rather than a new
sandbox-ctp-{env} client. The client-credentials token is subject to the same
audience, role, structural-group, and ownership rules as a user token.
Using another Keycloak client
Sandbox is not CTP-only. An owning team can onboard an existing Keycloak client by requesting these Identity/Sandbox changes:
- Add its client ID to
SERVICE_KC_ALLOWED_CLIENT_IDSfor the target environment. - Ensure its tokens contain the
sandbox-{env}audience. - Assign one of
org_admin,project_admin,writer, orvieweron thesandbox-{env}client. - Assign the same authority through an exact Sandbox structural group.
For a service account, assign the role and group to the service-account user, then
use that service's existing client_credentials flow. Only an explicitly approved
realm platform_admin is global.
Using Sandbox without Keycloak
External or transitional consumers may use a platform API key (X-Api-Key) with
explicit X-Sandbox-Tenant-Id / X-Sandbox-Project-Id headers. Prefer Keycloak for
CTP/canvas. When KeyHive is enabled, optional team tokens may authenticate; when
SANDBOX_LLM_EGRESS=gateway, prefer Keycloak / platform API keys
(ADR 0020).
Platform API keys and Keycloak tokens authenticate Catalog, Harbor, Agent, CodeEdit,
and Sandboxes. LLM BYOK is provisioned on Sandbox Admin credential-plane when
KeyHive is on, or on LLM Gateway when egress is gateway. See
Provisioning credentials.
Project scoping
X-Sandbox-Tenant-Id and X-Sandbox-Project-Id select the project a request is
billed and rate-limited against. They are validated against your credential rather
than taken at face value:
- A platform API key has no namespace of its own, so its headers are accepted as given.
- A Keycloak token may select only a tenant/project present in its exact structural role groups. Its role must also permit the requested resource/scope.
Who can see a job
Authenticating gets you in; it does not get you someone else's run. A job is visible only to a request whose resolved tenancy equals the tenancy it was submitted under. That covers every job-keyed route — status, events, cancel, retry-errored-trials, artifact listing, archive download, verifier stdout.
A job you may not see returns 404, identically to a job that does not exist. A
403 would confirm the id is real, which is the thing being protected, so the two
are deliberately indistinguishable.
| Credential | Can read a job belonging to acme:proj1 |
|---|---|
Platform key sending X-Sandbox-Tenant-Id: acme and X-Sandbox-Project-Id: proj1 | Yes |
| Platform key sending no tenancy headers | No — 404 |
Keycloak viewer/writer assigned to acme:proj1 | Yes |
| Keycloak role assigned only to another tenant/project | No — 403 while resolving scope, or 404 for a scoped record lookup |
So a platform key keeps its privilege of acting for any tenant, but it has to say which one — it cannot reach a run by knowing the job id alone. Internal tooling that polls across tenants must send the tenancy headers for the job it is asking about.
Jobs submitted with no tenancy at all are visible only to requests that also send none, which is what keeps single-tenant deployments and the local stack working.
Migration note: a platform key used to read any job from its id alone. A
dashboard that polls bare job ids will see 404 for jobs submitted with a tenancy
— including its own, if it stamps tenancy on submit but not on poll. Send the same
X-Sandbox-Tenant-Id and X-Sandbox-Project-Id on the poll as on the submit.
Because the denial looks like "no such job", a sudden run of 404s on ids you
believe exist usually means missing tenancy headers.
You never send a model provider key
Your Sandbox credential authenticates you to the compute platform. It is not the Anthropic/OpenAI/Google key the agent uses, and Sandbox will not read one out of your payload.
Workers obtain model credentials from SANDBOX_LLM_EGRESS:
gateway— LLM Gateway (Keycloak M2M). Sandbox/admin/credential-plane/*returns HTTP 410. Configure BYOK on LLM Gateway.direct(KeyHive) — lease from KeyHive; provision via Sandbox Admin credential-plane (admin key).
How to set compute quotas vs BYOK: Provisioning credentials.
What is not authenticated
/healthz,/readyz,/metrics— liveness and scrape endpoints/docs— this documentation site/openapi/sandbox-platform.yaml— the API specification
Everything under /sandbox/ needs a credential, including /sandbox/healthz. A
401 from that path is the gate working, not an outage; use /healthz for
unauthenticated health checks.
Local development
The local stack ships a placeholder API key (dev-local-key). Default compose uses
SANDBOX_LLM_EGRESS=gateway. For KeyHive locally, enable the keyhive compose
profile and set the KeyHive triad (see SECRETS).
For Gateway mode, configure LLM Gateway M2M when exercising model runs.
Next
- Provisioning credentials
- Core concepts — tenancy in context
- Errors and limits