Skip to main content

MCP security

This page lists what the MCP gateway of the Control Plane enforces on every MCP call, how credentials, provenance and suspension are handled, the threats this covers, and the steps a developer and a tenant administrator follow to harden an MCP server. It is written for developers, tenant administrators and security officers.

Checks of the gateway​

POST /v1/mcp/gateway/{server_id} runs the checks below in this order, for each message of a batch. A refusal never reaches the server and is audited.

OrderCheckRefusal
1Authentication of the caller, OpenFGA relation can_invoke on the server, operations roleHTTP 401 or 403
2Message is JSON-RPC 2.0-32600
3Server active-32003 "MCP server disabled"
4Isolation per project (ADR-0165)-32003 mcp_server_project_mismatch
5Access list of the server (allowed_principals; owner and agents for an MCP Runtime instance)-32003 mcp_principal_not_allowed
6Credential bound to an application: the server must be one of its mcp_servers-32003 with the code of the refusal
7Method among initialize, tools/list, tools/call, resources/list, resources/read, prompts/list, ping-32601
8tools/call: tool in allowed_tools (glob), scope tools:invoke-32003
9tools/call: DLP of the arguments with the tenant rules; a block rule refuses-32003 dlp_blocked
10Policy decision mcp.call (OPA) with method and target, kill switches of the SOCHTTP 403
11tools/call: quota reservation of one tool callHTTP 429 quota_exceeded
12resources/read: URI in allowed_resources (glob)-32003
13Egress: internal address only, checked again at connect timeHTTP 403 mcp_online_forbidden
14Answer: tools/list and resources/list filtered by the allow lists; tools/call result masked by the DLP; tools/call and resources/read results screened for prompt injection-32003 dlp_blocked or prompt_injection_blocked
15Usage event (tool call, duration) and audit event mcp.<method> with the target and the DLP findings
note

When the Tool Gateway calls an MCP tool (POST /v1/tools/{name}/invoke), it evaluates tool.invoke and meters the call itself; the MCP gateway then skips its own policy decision, quota and usage event.

Egress: internal servers only​

  • A server is reached at an internal address only (ADR-0170): an internal host of EAI_MCP_INTERNAL_HOSTS, a server hosted by the Navin Manager (its instances named in EAI_EGRESS_PRIVATE_ALLOWLIST), or an MCP Runtime instance at its in-cluster proxy address.
  • The address is checked when the server is registered and again at every call: every address the host resolves to must be private. A public destination answers 403 mcp_online_forbidden, is audited as security.egress.denied and counted in eai_mcp_online_refused_total.
  • The egress restriction of the tenant (allowed hosts) still applies.
  • The address of an MCP Runtime instance is written by the lifecycle controller, never by a tenant, and must match the in-cluster proxy pattern; the CiliumNetworkPolicy of the instance limits who reaches the proxy and where the MCP container may go.

Credentials through the broker​

  • Registration takes references only: credential_ref is openbao://... or env://...; auth.type connector names a connector whose credentials the Credential Broker holds.
  • The gateway resolves the secret at call time, server side, and puts it in the header (bearer, api_key) or in the query parameter (query_param) of the outbound request only. Answers return auth.type, header, param, credential_ref and connector, never a value.
  • A server hosted by the Navin Manager gets its own token, kept at openbao://mcp/hosted/<id>; no answer and no audit event holds it.
  • An MCP Runtime instance receives a short JWT per call (subject the caller, audience the instance) and its own temporary credentials, revoked when it sleeps or ends. See MCP Runtime per user and project.

Signature and provenance​

ContentProof
Tool schemassha256 of description, input and output schemas per tool; each new or changed schema is a catalogue version with an approval (mcp_tool_version)
Marketplace packs with MCP serversPublication gates: signature, SBOM, vulnerability scan, provenance, licence, permissions, tests (Marketplace)
Images of the MCP RuntimeInternal registry of EAI_MCP_RUNTIME_IMAGE_REGISTRIES, pinned by digest; Kyverno refuses an MCPServer without digest
Presets of the Navin librarysha256 digest of every item in catalog/navin/index.json, checked by python tools/navin_library/sync.py --check

Quarantine and suspension​

MechanismEffectHow
Disable a serverCalls answer "MCP server disabled" at once; enabling again is a promotion request approved by a second personPOST /v1/mcp/servers/{server_id}/status
Suspend a marketplace installationIts tools blocked, its agents suspended, its MCP servers disabled; resuming needs a four eyes approvalPOST /v1/marketplace/installations/{installation_id}/status
Kill switch of a packageEvery installation of the package suspendedPOST /v1/marketplace/packages/{name}/status
SOC order DISABLE_TOOL on target mcpPolicy decisions touching the server refused, across restarts of every replicaPOST /v1/soc/orders (SOC runbook)
Detection mcp_compromisedSeverity sev1, playbook DISABLE_TOOL on the MCP targetSOC detections
Hosted server no longer hostedDisabled at the next syncPOST /v1/mcp/hosted/sync
Failing MCP Runtime instanceDestroyed at once, credentials revokedLifecycle controller

Threats​

ThreatControlEvidence
Data of a project sent to an online MCP serviceInternal addresses only, checked at registration and at every callAudit security.egress.denied, eai_mcp_online_refused_total
Agent of one project calling a server of another projectIsolation per project, refusal before any upstream callAudit mcp.call with deny and the reason
Member of a project using the credentials of another memberAccess list per server, one MCP Runtime instance per user with its own credentials and Cedar policyAudit mcp.call principal_not_allowed
Rug pull: a server changes a tool description or schemaNew catalogue version with approval at each discovery; allowed_tools enforced by the gatewaynew_versions of the discovery, approvals
Tool poisoning or prompt injection in a resultUntrusted content screening of tools/call and resources/read resultsprompt_injection_blocked
Exfiltration of sensitive data in arguments or resultsDLP with the tenant rules: arguments inspected (block rules), results maskeddlp field of the audit event
Secret leak through the registryReferences only; secrets resolved server side at call timeauth of the answers without value
Replay of a token on another instanceOne audience per instance, short lifetimeEAI_MCP_RUNTIME_TOKEN_TTL_SECONDS
Abuse of callsQuota reservation per tool call, usage events429 quota_exceeded, usage
Compromised serverDisable, SOC kill switch, marketplace suspensionKill switch state, audit
Tampered image or packDigest pinning, signature and provenance gatesMarketplace gates, Kyverno

Harden a server and its project​

  1. Keep the allow lists short. Register the server with the exact allowed_tools and allowed_resources it needs, never * in production.

    Expected result: GET /v1/mcp/servers/{server_id}/tools lists only those tools.

    If it fails: the list cannot be edited in place; delete the server and register it again.

  2. Restrict the callers. PUT /v1/mcp/servers/{server_id}/access with the users and agents that need it.

    client.mcp.set_access("files-server", ["agent:support-bot", "user:alice"])

    Expected result: any other caller gets mcp_principal_not_allowed.

    If it fails: 409 mcp_instance_access_managed for an MCP Runtime entry (owner and agents only).

  3. Attach the server to its project. Register it with project; a server without a project is shared by the whole tenant.

    Expected result: the server is not listed from another project.

    If it fails: 403 mcp_project_forbidden when the body and the request name different projects.

  4. Set DLP block rules for the data the tool must never receive. Use the DLP rules of the tenant (GET /v1/dlp/rules, POST /v1/dlp/rules); the gateway applies them to the arguments and the results.

    Expected result: a tools/call with a blocked value answers dlp_blocked.

    If it fails: test the rule with POST /v1/dlp/inspect.

  5. Write a policy for mcp.call. Deny by default, allow the methods and tools the project needs, test each rule (Policy development).

    Expected result: an unlisted tool is refused with HTTP 403 by the policy decision.

    If it fails: run the policy tests (enterprise-ai test authz).

  6. Approve tool versions after a review. Read the schemas of each pending_approval version in GET /v1/mcp/catalog before approving.

    Expected result: every version in use is approved.

    If it fails: reject the version and disable the server until it is fixed.

  7. Watch the signals. Follow eai_mcp_calls_total by outcome, eai_mcp_online_refused_total and the audit events mcp.*; check the integrity of the audit chain with enterprise-ai audit verify.

    Expected result: refusals are explained by a known change.

    If it fails: open an incident and follow the SOC runbook.

danger

Never put a secret value in a registration body, a manifest or a preset field: secret fields accept references only, and a value written there would be readable by every reader of the registry.

Verify​

  • A call to a tool outside allowed_tools answers -32003 and GET /v1/audit/events holds mcp.tools.call with deny and not in allow list.
  • A registration with a public URL answers 403 mcp_online_forbidden.

Next steps​

Troubleshooting​

SymptomCauseFix
-32003 dlp_blocked on a legitimate callA tenant block rule matches the arguments or the resultCheck the findings in the audit event; adjust the rule with the security officer
-32003 prompt_injection_blockedThe result of the server holds instructions aimed at the modelFix the content served by the tool
HTTP 403 on every call after an incidentSOC kill switch or suspension activeRead GET /v1/soc/kill-switches; recovery follows the SOC runbook
HTTP 403 mcp_online_forbiddenPublic hostHost the server internally
-32003 "scope tools:invoke required"Key or token without the scopeIssue a key with tools:invoke