What the gateway trusts, what it checks on every request, what it records, and what an operator must put around it. Every statement here names the config or code path that enforces it; the review that produced this page and its findings are summarized at the end.
| Party | Holds | May |
|---|---|---|
| API customer | an access key (access_keys[].ak, or one created through the admin API) |
call the model, MCP and batch surfaces its key is entitled to, within its quotas |
| Tenant admin | the tenant's admin_token_env bearer |
read and change its own tenant's keys, usage, content-safety events; nothing global |
| Operator | the global admin.token_env bearer, the process environment, the config source |
everything: config publish and reload, every tenant, /internal/*, the upstream credentials |
| Upstream vendor / MCP server | its own endpoint | is trusted only to answer; its errors are mapped, its headers are filtered, its credentials never leave the process |
The gateway never holds a customer's vendor credentials: every upstream key, Bedrock token, MCP bearer and OAuth client secret is read from the environment variable the config names, at call time, and appears in no response, log line, audit row or trace.
- Access keys ride as
Authorization: Bearer <ak>orx-api-key: <ak>; a missing or unknown key is a401. Keys have a lifecycle:expires_at_epoch_secs,banned, and an automatic abuse suspension (abuse.tiers) each fail authentication with a distinct403, on every surface (REST, realtime — where the key is re-checked per turn — batches, MCP). Logs, traces and the ledger carryak_id, a SHA-256 fingerprint of the key, never the credential. - Admin routes are absent (
404) until an admin token is configured. Two tiers apply: the global token (admin.token_env) manages everything; a tenant'sadmin_token_envtoken manages only that tenant. A tenant token on a global-only route (reload, config publish,/internal/*, the cross-tenant/admin/audit/opstrail) is a403; a tenant token looking up a foreign key gets an empty page or a404, never a confirmation the key exists. - The web control plane keeps its own identity store and browser sessions and reaches the gateway only through the admin API with the tokens above.
- A key belongs to one tenant (the implicit
defaulttenant when undeclared). Model entitlement (tenants[].models) is enforced before the cache and the engine, so a tenant cannot be served another tenant's cached answer or reach a model it is not entitled to — including through a fallback chain, where unentitled entries are skipped. - Files, batches and retained content are tenant-owned: a foreign id answers
404, not403, so ids cannot be probed. - MCP: a key reaches only the servers named in its
mcp_serversentitlement (any other name answers404), and only the tools in itsmcp_toolsallowlist;tools/listis filtered to the allowlist and atools/calloutside it is refused inside the gateway (JSON-RPC-32000). An MCP session id is bound to the key that first received it. Keys created through the admin API carry no MCP entitlement. Only a fixed set of MCP headers crosses the proxy in either direction; the caller'sAuthorizationnever reaches the server and the server's headers never reach the caller beyondContent-TypeandMcp-Session-Id. The proxy's HTTP client follows no redirects, so a server or token endpoint cannot steer a credentialed request elsewhere. JSON-RPC batches are refused.
- Per-tenant
securitypolicy: blocklist (block / flag / shadow), regex recognizers, secret detection, DLP redaction of emails and phone numbers in both directions (a redacted stream is buffered and replayed so no unmasked span leaves), and an external moderator (moderation:, AWS Bedrock Guardrails) over inbound text and — for the MCP proxy — over tool results (tools/call,resources/read,prompts/get), reviewed whatever the reply's HTTP status and including a JSON-RPC error's message; a tenant under review cannot open an MCP listen stream, whose server-pushed content the proxy cannot review. Signed thinking blocks are never rewritten; a mask that would land in one fails the request instead. - A tenant admin may ban its own keys but can neither lift a ban nor change an abuse suspension: those are platform sanctions the global token owns.
- Every hit is a security event (
/admin/audit/events) carrying the rule, action and hit count — never the prompt text. Admin mutations land in/admin/audit/opswith the source IP (the TCP peer; the rightmostx-forwarded-forhop only undertrust_proxy_headers). Retained content (tenants[].retention) is sealed at rest withGW_CONTENT_KEY; without the key,fullretention degrades to redacted text.
Stated so an operator can choose them deliberately:
- Redis unreachable: rate limits, quotas and budgets fail open and a warning is logged; account health treats every account as healthy.
- Moderator unreachable:
security.moderation_fail_opendecides —false(default) denies the request or tool result,trueadmits it. - Upstream failure: account failover within the model, then the model's
fallback_modelschain, only while nothing has been sent to the client. Upstream and identity-provider errors reach the customer as a generic502; endpoints and error text stay in the gateway log. - Abuse of long-lived connections: realtime sessions and MCP listen streams
are capped per key (
max_live_streams_per_key); realtime turns and every MCP request spend the key's QPS permits; replies the proxy must buffer are capped per server (max_reply_bytes);x-gw-useris capped at 256 bytes, since it keys governance counters. - Billing store unreachable: rows queue in a bounded repair queue and apply backpressure rather than being dropped.
- Terminate TLS in front of the gateway; it listens on plain HTTP.
- Keep
/admin/*,/internal/*and/metricsoff the public load balancer (the sample nginx config in multi-instance does this) and leaveGW_ADMIN_TOKENunset on instances that do not need the admin API. - Put MCP servers on a private network the gateway alone can reach; the gateway is their only client and holds their credentials.
- Give each customer their own key with
qps,daily_token_quota,tokens_per_minuteand a cost budget, and route alerts (alerts.webhook_url_env) somewhere watched. - Rotate upstream and MCP credentials by changing the environment and reloading; OAuth tokens the gateway fetched are dropped on reload.
A targeted adversarial review of the admin plane and the MCP proxy ran on
2026-09-09 against three lenses: authentication and tenant scoping;
injection, smuggling and protocol edge cases; resource exhaustion and
information leakage. Fixed in the same change: unbounded buffering of MCP
replies, redirect following by the MCP client (credential replay and SSRF),
upstream endpoints and identity-provider error text in customer-visible
errors, resources/read and prompts/get results escaping review, multi-line
SSE data and other unparsable replies passing through unreviewed, structured
tool output escaping a mask, stream resumption replaying results past the
review, unbound MCP session ids, tool calls audited only after the upstream
answered, a tools/call without a string tool name skipping every gate,
realtime sessions and listen streams with no per-key cap, in-process monthly
counters keyed by x-gw-user never evicted, a tenant token lifting bans and
suspensions — through the key-create route as well as PATCH — raw access keys
in denial messages, admin listings without a page ceiling, and MCP server names
probe-able through 404-vs-403. A second pass over the same surface fixed:
results escaping review on a non-2xx status or when carried as a JSON-RPC
error; prose in structured tool output under identifier-named keys, while
base64 image and blob payloads are left untouched; the attribution hint
uncapped on the realtime subprotocol path and in request and batch bodies; the
whole fallback chain disabled for any reasoning request rather than only one
replaying reasoning output; a listen stream handing unreviewed server-pushed
content to a tenant under review; a zero max_reply_bytes refusing every
reply and a zero token lifetime defeating the token cache; and one server's
slow token endpoint stalling another server's first token fetch.
Accepted as is, with the reason: the ledger, security-event and retained-
content tables store the access key itself (the ledger joins usage by it;
database read access sits inside the operator boundary — protect the database
like the config); the listener has no header-read or idle timeout of its own
(a proxy in front terminates slow clients, which the deployment guidance
requires anyway); a video download is held in memory for the clip's size; a
config reload can race one in-flight token fetch for at most one token
lifetime; duplicate JSON keys parse last-wins here and possibly first-wins on
a non-compliant upstream (a compliant server behaves identically); the
cross-tenant key guard reads a two-second key cache before mutating, which
would need a key to change tenant inside that window; MCP session binding is
enforced per instance, so a multi-instance deployment must route a session's
requests to the instance that opened it (sticky by Mcp-Session-Id), which
Streamable HTTP already assumes; a server that returns one constant session id
locks its session to the first key to use it (a conformant server mints a
unique id per session).