Skip to main content

How the managed gateway works

The managed gateway is the policy enforcement point (PEP) built into the PolicyArc authorization server. It runs in the same process, on the same address, so there is nothing extra to deploy. It sits in front of each managed resource, asks the authorization server whether a call is allowed, and forwards only the calls that are.

The gateway holds no policy. On each call it sends the caller's token and the details of the call to /introspect. Policy runs there, and the gateway acts on the answer.

Two ways in​

PathWho calls itWhat it reaches
POST /gateway/mcpMCP clientsEvery MCP tool of every enabled managed resource, in one MCP server. A tool is named <resource>__<tool>.
/gateway/<name>/...Any HTTP clientOne REST resource. The rest of the path is forwarded to the upstream.

A managed resource has one of three upstream types. A REST resource proxies to an HTTP API, and a route with an mcp block is also an MCP tool. An MCP resource forwards tool calls to an MCP server and is reachable only through /gateway/mcp. A STATIC resource has no upstream. It runs every check and then returns a response written into the route. Connectors create these resources for you, and Register a managed resource shows how to create one yourself.

What happens on each call​

The gateway takes these steps in order. A step that fails ends the call. Before the first step, a REST call that carries a token and a body larger than pbac.gateway.max-body-size (PBAC_GATEWAY_MAX_BODY_SIZE, 10 MB by default) is refused with 413.

  1. It finds the resource by name, and for a REST call, the first route that matches the method and path.
  2. It checks that the request carries a token.
  3. It calls /introspect with the token and the call's context: the scope the route needs, the resource type, the resource name, the tool name, the tool arguments, and the interface the call came through (mcp or api). Policy sees this context as input.context.rs_context.
  4. If the token is bound to a DPoP key, it checks the DPoP proof on the request.
  5. It refuses a token that was anchored by resource indicator to another endpoint of the same resource type. It does not refuse a token of another type whose scope names match the route's scope. See Check the scope and resource.
  6. If the upstream needs the user's own credential and none is stored, it returns a link for the user to connect one. See Per-user upstream credentials.
  7. It builds the upstream credential from the resource's upstreamAuth.
  8. If policy asked for a file check, it looks up the file and denies the call before it reaches the upstream. See File access control.
  9. It forwards the call. It does not forward the caller's Authorization, Cookie, X-Admin-API-Key, X-Bundle-API-Key, DPoP or Mcp-Session-Id headers, the Forwarded, X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto and X-Real-IP headers, or the method-override headers X-HTTP-Method-Override, X-HTTP-Method and X-Method-Override. Other caller headers are forwarded.
  10. It applies the obligations policy returned with the decision, such as redacting words in the response. See Obligations.
  11. It writes an audit row and returns the response.

What each status means​

StatusBody or reasonWhen
400Resource is not MCP-typedOnly through /gateway/mcp. A call meant for an MCP upstream named a resource whose type is not MCP.
401No tokenThe request has no bearer token. The response carries WWW-Authenticate: Bearer resource_metadata="<environment URL>/.well-known/oauth-protected-resource".
401Invalid or expired tokenIntrospection says the token is not valid. WWW-Authenticate adds error="invalid_token".
401idp_link_required or user_secret_requiredThe user must connect an account or enter a credential first.
401A DPoP error, such as dpop_proof_required: ...The token is DPoP-bound and the request has no valid DPoP proof for that key.
403No matching routeThe resource has routes and none matches the method and path.
403Access deniedPolicy refused the call, or an obligation denied it. A token whose grant for the type is limited to MCP is refused on a REST call with the policy reason interface_not_granted.
403Token was issued for a different endpointThe token was issued for another endpoint of the same resource type. The audit reason is token_audience_mismatch.
403Introspection failedThe call to /introspect failed. The gateway does not forward a call it could not check.
403Resource missing default_scopeOnly through /gateway/mcp. The MCP resource has no defaultScope, so the gateway cannot check the call.
404Unknown resource: <name>No enabled resource has that name.
404Resource is not HTTP-accessibleThe resource is an MCP resource. Call it through /gateway/mcp.
413Request body too largeA REST call's body is larger than pbac.gateway.max-body-size.
500Resource not fully provisionedThe gateway has no usable credentials for the resource's introspection client.
502Upstream auth failed or Upstream request failedThe gateway could not get an upstream credential or reach the upstream.

Through POST /gateway/mcp, a missing or invalid token, or a failed DPoP check, is a real HTTP 401 with a WWW-Authenticate header. Other statuses from the gateway are not. The response is HTTP 200, and the tool result has "isError": true and text such as HTTP 403: Access denied.

What it records​

Each REST call to /gateway/<name>/... and each MCP tool call to a managed resource writes an audit row whose endpoint is /gateway/<name>/<path>, or /gateway/<name>/tools/call:<tool> for an MCP resource. A denied call carries a reason such as no_token, no_matching_route, pdp_deny, token_invalid or upstream_request_failed. A pdp_deny row also carries pdp_reason, the reason from policy. The /introspect call writes its own row just before, with the full policy input.

These do not write a gateway row. An MCP initialize. A request to /gateway/mcp with no token. An MCP tools/list, which writes an /introspect row for each MCP resource whose tools it fetches.

The two built-in tools, policyarc__list_my_upstream_connections and policyarc__reconnect_upstream_credential, act only on the caller's own connections and do not call /introspect. Each call writes a row with the endpoint /gateway/mcp and a reason such as connection_list_requested, user_secret_reconnect_requested or idp_reconnect_requested. We read this in the code and did not run it.

Obligations​

An obligation is an instruction that comes back with an allow, telling the gateway what else to do with the call. The gateway applies filter_words, audit, filter_file_access, require_file_check and apply_label. It ignores an obligation name it does not know. If a handler that changes the response body fails, the gateway denies the call with the reason obligation_failed rather than return the unchanged body. Obligations shows how to emit one from policy.

Per-user upstream credentials​

Some upstreams accept only a credential that the user holds, such as a Jenkins user name and API token. For these, a connector uses the user_secret upstream auth type. Each user enters their credential once, and the gateway sends it on that user's calls.

The connector lists the fields to collect and a header template.

"upstreamAuth": {
"type": "user_secret",
"fields": [
{"name": "username", "label": "Jenkins user", "type": "text", "required": true},
{"name": "api_token", "label": "API token", "type": "password", "required": true}
],
"headers": {"Authorization": "Basic {{basic field.username field.api_token}}"}
}

In a header template, {{field.<name>}} is the value of one field. {{basic field.<a> field.<b>}} is the Base64 of <a>:<b>, for HTTP Basic.

  • On the first call with no stored credential, the gateway returns a link to a form at /gateway/connect on the environment. The user enters the credential in the browser. It goes to the authorization server only, never through the MCP client or the model. The server stores it encrypted, per user and resource.
  • If the upstream later answers HTTP 401, the gateway asks the user to connect again. An HTTP 403 from the upstream is passed back unchanged.
  • An administrator removes a user's stored credential with DELETE /admin/api/managed-resources/<name>/user-secrets/<subjectId>. That only deletes PolicyArc's copy. Revoke the credential at the upstream too.

We read this flow in the code. We have not run it end to end for this page.

Discovery​

EndpointWhat it returns
GET /.well-known/oauth-protected-resourceThe resource <environment URL>/gateway/mcp, the authorization server, and scopes_supported, which lists the scopes offered to self-registered MCP clients.
GET /.well-known/oauth-authorization-serverThe authorization server metadata, on the same origin.

MCP protocol flows shows how an MCP client uses them to sign in.

The remote gateway​

The repository also has pbac-gateway, a separate Python gateway. It is a generic PEP for an API that the environment does not manage, such as an MCP server inside a customer network. It reads its own gateway.yaml, does not read connector manifests, and asks the authorization server through /introspect like the managed gateway does. Everything on this page is about the managed gateway. Use the managed gateway unless the upstream must sit behind a PEP that runs outside the environment.