Identity and authorization¶
MCP Runtime uses separate identities for platform administration, delegated agent access, and Kubernetes workloads. Each identity defines who is acting, what they may do, and which component enforces the decision.
The three identity planes¶
| Identity plane | Identity shape | Used for | Enforced by |
|---|---|---|---|
| Platform | User or service principal with role, subject, teams, and namespaces | UI, CLI, and platform API operations | platform-api, runtime-api, and analytics-api |
| Agent governance | humanID + agentID + teamID + sessionID |
MCP tools/call authorization |
MCP gateway |
| Kubernetes workload | ServiceAccount plus RBAC bindings | Reading and changing cluster resources | Kubernetes API server |
Each plane is enforced independently. A platform administrator does not automatically have an agent session, and an MCP agent identity is separate from any Kubernetes ServiceAccount.
Platform identity: who controls the platform¶
platform-api, runtime-api, and analytics-api authenticate a request using one of these credentials:
- A browser or CLI bearer token issued after local or OIDC login
- A user-owned API key sent as
x-api-key - A configured service or administrator API key sent as
x-api-key
Authentication produces a platform principal containing fields such as:
role
subject / user ID
email
primary namespace
allowed namespaces
team memberships and team roles
authentication type
The API uses this principal to authorize control-plane actions.
| Actor | Allowed actions | Why |
|---|---|---|
| Anonymous caller | Health, login, OIDC exchange, and explicitly public catalog reads | These routes are public by design |
| Authenticated user | Read visible teams, namespaces, servers, and deployments, plus team-scoped analytics on GET /api/v1/user/analytics/usage |
Visibility is scoped by the principal's namespaces and teams |
| Server owner | Change their server and administer its grants and sessions | The server carries the owner's platform user label |
| Team owner | Manage team members and administer servers and access resources in the team's namespace | Team-owner membership grants administrative authority within that team |
| Platform administrator | Manage teams, users, namespaces, platform operations, and resources across tenants, and read the unscoped analytics routes (/api/v1/events, /stats, /sources, /event-types, /analytics/usage) |
The admin role is the platform-wide control-plane authority |
| Service API key | Only the API operations permitted by its assigned role | Service authentication does not automatically create a human or agent identity |
Creating or changing an access grant requires authority over the referenced server. In normal platform flows, that means the caller is the server owner, a team owner for the server namespace, or a platform administrator.
Direct creation of an MCPAgentSession through /api/v1/runtime/sessions is
administrator/internal-only. Normal users obtain a session through the adapter
session endpoint.
See the complete endpoint matrix in API authn/authz matrix.
Trusted ingress and client IP¶
The API derives the caller's client IP from the left-most X-Forwarded-For
hop, falling back to RemoteAddr. This IP is used both for audit ActorIP
labelling and for login-lockout bucketing (brute-force throttling on
/api/v1/auth/login). Because clients can set X-Forwarded-For freely, the
ingress in front of platform-api must set/overwrite this header
authoritatively and strip any client-supplied value. Otherwise a caller can
rotate the header to evade lockout or poison another address's bucket.
Configure Traefik (and any upstream load balancer) so platform-api sees the
real client address, and so inbound X-Forwarded-For from untrusted clients is
discarded, not appended. The ingress is the only trusted boundary for client
IP; platform-api uses the header value it receives.
Agent identity: who is using an MCP tool¶
An agent call carries a delegated governance identity:
humanID the platform user responsible for the call
agentID the agent or client acting for that user
teamID the stable team whose server and policy are in use
sessionID the active platform-owned session name
The adapter obtains a session-bound certificate from the platform. Traefik verifies it and the gateway resolves its SPIFFE URI against its synchronized platform session cache. Caller-supplied identity headers are never accepted as authentication.
The platform agent directory gives each managed AgentID a stable record
owned by one team. Its display name and active status help administrators
select and govern agents in grants and sessions. The directory is governance
metadata; the ID alone does not authenticate a runtime or prove which software
made a request. Authentication comes from optional OAuth for direct clients
and from the session-bound certificate for enrolled adapters. OAuth-enabled
adapter targets require both.
In the dashboard, open Agents from the top navigation. Platform admins can search and page across all teams or select one team. Team owners can create, rename, deactivate, and reactivate agents in their team. Members see only active agents covered by an applicable grant or their own active session. Open an agent to see its stable ID, owning team, status, applicable grants, and session history. The detail view shows grant subjects and session human IDs where those records provide them; agents have no separate user owner. Managers can grant access from the detail view with the agent and team already selected, change grants, and revoke sessions in namespaces they manage. An inactive agent stays in the directory with its history but cannot receive a new grant or session. Team and membership views link to the matching Agents view.
Access changes require a platform admin or team owner. Members can inspect
their own applicable access and sessions, but the dashboard has no access
request submission workflow yet. Ask a team owner outside the dashboard for
additional access; no request is created by viewing an agent. After a grant,
sign in with mcp-runtime auth login --api-url <platform-url>, then run
mcp-runtime adapter proxy --runtime-url <https-mcp-url> --server <server> --namespace <namespace> --agent <agent-id> --auto-refresh. The proxy requests
an authorized session and enrolls a certificate in memory. For a saved
certificate, use mcp-runtime adapter enroll with the same server, namespace,
and agent. The private key stays local; an OAuth-enabled target also needs its
OAuth bearer token. A grant alone does not establish a connection. See
Agent adapters for the supported CLI steps. Direct MCP
clients remain governed by their existing gateway policy.
Agent IDs are platform-generated immutable agt_<26-character lowercase
ULID> values. Agent subjects must resolve to an active directory record owned
by the selected subject team. Unknown, malformed, inactive, or wrong-team IDs
are rejected, and agent IDs cannot be entered as free text in the admin access
forms. The runtime API checks grants, sessions, adapter session issuance, and
certificate enrollment; use the runtime API for access changes so it can
validate agent ownership against the identity store. Direct Kubernetes CRD
writes do not consult the identity store and therefore bypass these directory
checks; adapter session issuance and certificate enrollment still verify the
agent before they issue credentials.
Platform-api marks the agent inactive and revokes its active sessions in one
Postgres transaction. Session issuance locks and validates the same directory
record, so a concurrent enrollment cannot escape deactivation.
For audit, keep the actor (the agent named by the session) separate from
the authority (the human or team that delegated access). A session and its
gateway decisions should preserve both identities, along with the grant,
server, MCP method/tool, decision, and request trace ID. MCP's OAuth roles and
emerging workload identity guidance provide useful context; an OAuth client
registration or client_id does not automatically create a managed agent.
Direct clients use OAuth only when the target server configures spec.auth.
When MCP_ADAPTER_CERTIFICATES=true, an adapter presents its session-bound
client certificate during the HTTPS handshake; Traefik verifies the certificate
and the gateway resolves its session identity for grant/session authorization.
On an OAuth-enabled target, the adapter also forwards a bearer on each HTTP
request, and its subject must match the session human identity. Certificate
identity and OAuth are separate credentials. The ingress switch is opt-in.
Enrollment uses the platform-wide MCP_MTLS_CLUSTER_ISSUER and
MCP_TRUST_DOMAIN settings and does not by itself make Traefik request the
certificate.
For the adapter request flow, including how one certificate can accompany
different per-request OAuth tokens, see
Certificate identity and OAuth tokens.
Further reading: MCP authorization, WIMSE Agent Identity Management Services draft (work in progress), AUDIT delegation and interaction traceability proposal, and SPIFFE SVIDs.
Delegated header authentication¶
auth.mode: header is an explicit compatibility mode for an MCP server that
authenticates clients with its own credential headers. Runtime does not
validate those credentials and does not turn a header value into a human,
agent, team, or session. The gateway checks that the configured headers are
present, applies server and tool rules, forwards the headers only to that
server, and records the caller as unverified.
auth:
mode: header
credentialPresence: any
headers:
- X-Example-Credential
- Private-Token
- Authorization
policy:
mode: allow-list
defaultDecision: deny
delegatedToolRules:
- name: list_projects
decision: allow
credentialPresence: any accepts one or more of the configured headers and
forwards every present header. all requires every configured name. Neither
mode selects a preferred header. Header names are arbitrary valid HTTP tokens
except reserved transport, routing, and trusted identity headers.
The configured name list is the whole credential contract. The gateway does not recognize auth schemes or vendors. After the required configured headers are present, every listed header is forwarded unchanged for the MCP server to authenticate. The value is opaque. Runtime does not parse it or turn it into an OAuth identity.
Authorization is included only when the server lists that name. Otherwise
the gateway still removes it. Tool rules still run before the request is
forwarded: a denied tool never reaches the server.
Header mode requires allow-list enforcement and a default deny. Unlisted tools and tools without a declared side effect are denied before the upstream call. Grants, sessions, observation mode, and OAuth settings are rejected. A failed OAuth server does not fall back to this mode. Direct clients send the headers themselves. An optional adapter can inject them from local sources; that adapter path is separate and does not create a Runtime principal.
The server list and mcp-runtime server connect-config copy auth.headers
into the client snippet as a headers object with empty values. The console
Claude, Cursor, and VS Code tabs use that same object. Fill one value when
credentialPresence is any and delete the other empty headers, because an
empty header is rejected. all requires every listed header. The snippet
never contains a credential value.
Grant: administrator-approved authority¶
An MCPAccessGrant answers:
Which human, agent, or team may use which MCP server, with which tools, side effects, and maximum trust?
Its important fields are:
spec:
serverRef:
name: payments
namespace: mcp-team-finance
subject:
humanID: user-123
agentID: agt_01arz3ndektsv4rrffq69g5fav
teamID: team-finance-id
maxTrust: medium
allowedSideEffects: [read, write]
toolRules:
- name: create_invoice
decision: allow
requiredTrust: medium
Every populated subject field must match the request identity. A grant bound to
all three subject fields does not match a different human, agent, or team.
For cross-team access, subject.teamID names the grantee team while the
referenced MCPServer.spec.teamID remains the resource-owning authority team.
The grant does not transfer or rewrite server ownership. Cross-team subjects
must be active members of the named team, and their grant must expire within
MCP_CROSS_TEAM_GRANT_MAX_TTL (default seven days). Adapter sessions stop at
the grant expiry. Authorization and audit records preserve both dimensions:
the caller's subject team and the server's resource team.
For administrator-created sessions, pass grantName to link the session to an
active grant; the API verifies its server and every populated subject field,
then caps session trust and expiry to the grant. Grant-wide session revocation
targets sessions with this explicit link, including adapter-issued sessions.
Manually created sessions without grantName are not part of a grant's
revoke-all operation.
Resource namespace owners can look up one known recipient team through
GET /api/v1/runtime/delegation-recipients?namespace=<resource-namespace>&team=<exact-team-slug>.
The response contains only the team's ID/name, active agent identities, and
member user IDs. It does not grant access to global team listings or foreign
account metadata. limit defaults to 50 and is capped at 100; use
agent_cursor and member_cursor with the corresponding next cursors to
continue each list. Ordinary team members and unrelated namespace owners
cannot use this lookup for a resource they do not manage. Directory outages
return 503; invalid foreign subjects and invalid grant expiry return 422.
Grant-wide revocation is a single platform session-store transaction, scoped by the exact grant namespace and name, including migrated links in other namespaces. The response counts newly revoked sessions; repeating the action is safe and returns zero when no active linked sessions remain. Per-session audits and an operation audit preserve subject and resource teams. A store failure returns an error and requires a retry, without a successful revocation audit. An enabled grant can issue a fresh session later; its new identity does not reactivate a previously revoked certificate.
The grant controls:
- Whether a specific tool is allowed or denied
- Which side-effect classes are allowed
- The administrator-approved maximum trust
- Whether the grant is disabled
The grant does not prove that the agent has a current session.
Managed agent IDs are created with mcp-runtime agent create <team-slug>
--name <name> and selected from the team's agent directory when creating a
grant or session. The directory page and mcp-runtime agent list|get|rename|
deactivate|reactivate expose the same records. IDs are generated once and
never reused; do not invent an agent ID for a new managed subject.
Session: active delegated consent¶
A platform session, represented by MCPAgentSession YAML for CLI input, answers:
Is this human-agent-team identity currently active for this server, and how much trust is consented for this session?
Its important fields are:
spec:
serverRef:
name: payments
namespace: mcp-team-finance
subject:
humanID: user-123
agentID: agt_01arz3ndektsv4rrffq69g5fav
teamID: team-finance-id
consentedTrust: low
expiresAt: 2030-06-12T12:00:00Z
revoked: false
For the normal adapter flow:
- An authenticated platform user asks for a session for a server and agent.
- The platform verifies the user's team and namespace access.
- The platform selects a matching enabled grant.
- Requested trust is capped by the grant's
maxTrust. - The platform persists a new session or reuses a live matching session.
- The adapter presents the certificate and, when the target enables OAuth, forwards the bearer token.
The session controls expiry, revocation, and consented trust. It does not
contain tool rules or allowedSideEffects; those remain grant policy.
Session state lives in Postgres, owned by platform-api. The gateway synchronizes only its server UID's sessions through a separate, authenticated internal port; ordinary authorization uses its in-process cache. New adapter session names include a random suffix so a retired certificate cannot name a replacement session. Use the platform CLI/UI for session creation, inspection and revocation; Kubernetes session edits do not affect access.
The gateway polls changes every 500 ms. A cold gateway denies until the first
sync. A disconnected gateway retains its last complete snapshot for at most
ten minutes (configurable downward with SESSION_SYNC_STALE_WINDOW), then
denies. An epoch/cursor reset immediately invalidates the cache until relisting
succeeds. See session sync for restore
requirements and the policy schema v4 migration.
Tool metadata: what the server declares¶
When the MCP server is defined, its owner or deployer classifies each governed tool:
tools:
- name: list_invoices
requiredTrust: low
sideEffect: read
- name: delete_invoice
requiredTrust: high
sideEffect: destructive
The gateway uses the declared metadata as its policy input; it does not
inspect tool implementations. A tool that the server never declared, or whose
side-effect metadata is missing or unknown, is denied with
tool_side_effect_unknown.
Gateway decision for every tools/call¶
For a tool call, the gateway authorizes the intersection of identity, policy, consent, and tool metadata:
valid identity
AND valid matching session
AND matching enabled grant
AND tool allowed by the grant
AND tool side effect allowed by the grant
AND effective trust >= required trust
The decision ladder¶
The gateway extracts the tool name from the JSON-RPC request, loads the rendered policy for the target server, and walks these checks in order. The first failing rung wins, and its reason code is what lands in the audit event and the denial response. A denied call never reaches the MCP server.
| # | Check | Fails with |
|---|---|---|
| 0 | Is policy.mode: observe? If so, allow now; nothing below runs. |
(allowed, still audited) |
| 1 | Is there any human, agent, or team identity? | missing_identity (401) |
| 2 | With session.required: true: is there a session ID, a matching session, not revoked, not expired? |
missing_session, session_not_found, session_revoked, session_expired (401) |
| 3 | Does any grant's subject match? Every populated subject field must match exactly. | no_matching_grant |
| 4 | Does a tool rule deny this tool, or does no enabled grant allow it? Disabled and expired grants are skipped; a grant with no toolRules allows every tool name. |
tool_denied (403), tool_not_granted, grant_expired (every matching grant that is not disabled has passed its expiresAt) |
| 5 | Did the server declare this tool's side effect, and does the grant's allowedSideEffects include it? |
tool_side_effect_unknown, side_effect_not_allowed (403) |
| 6 | Does the grant carry a maxTrust? |
grant_without_trust |
| 7 | Is effective trust at least the required trust? | trust_too_low (403) |
| ✓ | Forward to the MCP server and emit the audit event. | allowed |
The trust comparison on rung 7 is:
effectiveTrust = min(grant.maxTrust, session.consentedTrust)
requiredTrust = max(tool.requiredTrust, matchingToolRule.requiredTrust)
When no session is required or none matches, consentedTrust falls back to the
grant's maxTrust.
Reasons without a status code (no_matching_grant, tool_not_granted, grant_expired,
grant_without_trust) follow policy.defaultDecision: 403 under the shipped
deny default, allowed only if a server explicitly sets defaultDecision: allow.
Every decision, allowed or denied, emits an audit event containing the identity, tool, decision, reason, trust values, server, namespace, and policy version.
Observe mode is reporting, not enforcement
policy.mode: observe allows the call without any identity, session, grant,
side-effect, or trust check; only the audit trail keeps visibility. Use it to
preview what enforcement would deny on a new server, then switch back to
allow-list.
Example:
Tool: delete_invoice
Declared effect: destructive
Declared trust: high
Grant tool rule: allow
Grant side effects: [read, write]
Grant max trust: high
Session consent: high
Result: deny, side_effect_not_allowed
Authorization is an intersection, so the allow rule alone is not enough. The
grant must also permit destructive.
Kubernetes workload identity¶
The operator and runtime-api use Kubernetes ServiceAccounts and RBAC to perform cluster operations. This identity plane controls actions such as:
- Reading and reconciling
MCPServer,MCPAccessGrant, andMCPAgentSessionresources - Creating workloads, Services, Ingresses, Secrets, and namespace-scoped access resources
- Reading platform configuration required by the service
Most workloads that do not call the Kubernetes API disable automatic ServiceAccount token mounting. Kubernetes RBAC is a separate enforcement layer from platform roles and gateway policy.
Responsibility summary¶
| Question | Source of truth |
|---|---|
| Who is operating the platform? | Authenticated platform principal |
| Which tenant resources can they manage? | Role, team membership, namespace scope, and ownership |
| Which human and agent are making the tool call? | OAuth claims and/or the certificate-bound grant/session subject |
| Is that delegated access active now? | Session expiry and revocation |
| Which tools can be called? | Grant tool rules |
| Which classes of effects are permitted? | Grant allowedSideEffects |
| How risky is the tool? | MCPServer tool metadata |
| How much authority is effective? | Minimum of grant maximum and session consent |
| Who enforces the tool call? | MCP gateway |
| Who can change cluster resources? | Kubernetes ServiceAccount and RBAC |
Next: Architecture shows where identity and policy are enforced across components. Use Teams and Access to apply the model to namespace boundaries and cross-team delegation.