Skip to content

security: harden the admin API and the MCP proxy - #72

Merged
CMGS merged 2 commits into
mainfrom
sec/mcp-admin-hardening
Sep 9, 2026
Merged

security: harden the admin API and the MCP proxy#72
CMGS merged 2 commits into
mainfrom
sec/mcp-admin-hardening

Conversation

@CMGS

@CMGS CMGS commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

A targeted security review of the admin API and the MCP proxy, run against the attack surface of the recent LiteLLM advisories (tenant-scoped tokens reaching foreign or global state; the proxy relaying results, redirects and errors it never inspected). Everything the review found reachable under the deployment contract is fixed here; what is accepted as is, and why, is written down in the new docs/security.md.

MCP proxy

  • Buffered replies (filtered tools/list, reviewed results) are capped by mcp_servers[].max_reply_bytes (default 16 MiB); a larger reply is a 502, never held.
  • The MCP HTTP client follows no redirects: a server or token endpoint cannot steer a credentialed request elsewhere.
  • Upstream and identity-provider failures answer a generic 502; endpoints and error text stay in the gateway log.
  • resources/read and prompts/get are reviewed like tools/call; multi-line SSE data is assembled before review; a reply the proxy cannot parse fails closed; a mask covers structuredContent; a denial replaces the whole reply with one JSON-RPC error (-32001).
  • A reviewed tenant gets no Last-Event-ID resumption, so a result cannot replay past the review.
  • Mcp-Session-Id is bound to the key that first received it; another key's use of it is a 404. Tool calls are audited before the upstream call. A tools/call without a string params.name is a 400. An unentitled server answers like an unknown one.

Admin API and shared surfaces

  • A tenant admin token can add a ban but can neither lift one nor change an abuse suspension (403); those are platform sanctions the global token owns.
  • Admin listings cap limit at 10 000.
  • Denial messages carry the key fingerprint (ak_id), not the key.
  • x-gw-user is capped at 256 bytes, since it keys governance counters; in-process monthly counters are swept to the current and previous month at the daily reset.
  • Realtime sessions and MCP listen streams are capped per key (max_live_streams_per_key, default 64, 0 = unlimited); the slot is released with the stream.
  • The fallback chain is not taken for a request that pins signed thinking to the model that produced it, and is taken on gateway 502/503 (connection failure, exhausted pool) as well as vendor 5xx/429.

Docs

docs/security.md (new): trust boundaries, authentication, tenant scoping, content controls, failure postures, operating recommendations, and the review record. api.md, configuration.md, governance.md, deployment.md, multi-instance.md, observability.md, architecture.md, index.md and the README are aligned.

Test plan

  • Unit and e2e: session binding, listen-stream cap, reply cap, positional tools/call, fail-closed parsing with framing preserved, masked everywhere or blocked whole, no resumption for reviewed tenants, tenant-token sanction refusal, realtime session cap.
  • Local: cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace (with GW_TEST_REDIS_URL on the Linux host).
  • Linux gates and a live run on the testbed: results follow in a comment.

CMGS added 2 commits September 9, 2026 17:50
A targeted review of the admin plane and /mcp/{server} against the
recent LiteLLM advisories. Fixed here:

- MCP replies the proxy must buffer are capped (max_reply_bytes), the
  client follows no redirects, and upstream or token-endpoint errors
  reach the caller as a generic 502 with the detail in the log.
- resources/read and prompts/get results are reviewed like tools/call;
  multi-line SSE data and unparsable replies fail closed; a mask covers
  structuredContent; a reviewed tenant gets no Last-Event-ID resumption;
  a denial replaces the whole reply with one JSON-RPC error.
- Mcp-Session-Id is bound to the key that first received it; a foreign
  session answers 404; tool calls are audited before the upstream call;
  a tools/call without a string params.name is a 400; an unentitled
  server answers like an unknown one.
- Realtime sessions and MCP listen streams are capped per key
  (max_live_streams_per_key, default 64) through a guard released with
  the stream.
- In-process monthly counters are swept to the current and previous
  month at the daily reset; x-gw-user is capped at 256 bytes.
- A tenant admin token can no longer lift a ban or change an abuse
  suspension; admin listings cap `limit` at 10000; denial messages
  carry the key fingerprint instead of the key.
- The fallback chain skips requests that pin signed thinking to the
  model that produced it, and is taken on gateway 502/503 as well as
  vendor 5xx/429.

docs/security.md records the trust boundaries, the failure postures and
the review ledger, fixed and accepted alike.
…n plane

An adversarial review round over the same surface turned up further gaps:

- MCP result review no longer depends on the reply's HTTP status and now
  covers a JSON-RPC error's own message, so a non-2xx or error-shaped reply
  cannot carry unreviewed prose past a reviewed tenant.
- Result review reads prose under every key except base64 binary (`blob`,
  `data`), so identifiers no longer shield text and an image is never
  rewritten.
- A tenant under `security.moderate` may not open an MCP listen stream, whose
  server-pushed content the proxy cannot review.
- A tenant admin can no longer lift a ban by re-creating the key: a platform
  ban or suspension on an existing key survives a tenant re-create.
- The 256-byte attribution cap applies on the realtime subprotocol path and to
  the request-body and batch-item user fields, not only the header.
- The model fallback chain is disabled only for a request replaying reasoning
  output, not for any request that merely engages reasoning.
- An upstream connection failure's cause is logged again while the client
  still gets a generic 502.
- max_reply_bytes must be greater than 0; a token endpoint reporting
  expires_in 0 no longer defeats the token cache; one server's slow token
  endpoint no longer stalls another server's first token fetch.
@CMGS
CMGS merged commit 053d3e8 into main Sep 9, 2026
2 checks passed
@CMGS
CMGS deleted the sec/mcp-admin-hardening branch September 9, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant