Gateway

Security model

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.

Trust boundaries

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.

Authentication

Authorization and tenant scoping

Content controls

Failure postures

Stated so an operator can choose them deliberately:

Operating recommendations

Review record

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 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 the instance’s key cache before mutating (with Postgres a key write invalidates it fleet-wide by NOTIFY within about a second), 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 sticky by Mcp-Session-Id (the id-less initialize may land on another instance, where ownership binds only from a reply that carries the id, so a second key holding the id could use the session there until one does); 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).