Skip to content

Commit 4249b3f

Browse files
authored
Merge pull request #110614 from ShaunaDiaz/OSDOCS-19231
OSDOCS-19231: adds tool revocation to MCP gateway docs
2 parents 35d949b + c9763af commit 4249b3f

5 files changed

Lines changed: 297 additions & 2 deletions

_topic_maps/_topic_map.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,10 @@ Topics:
8181
File: mcp-gateway-authentication
8282
- Name: Configuring authorization for the MCP gateway
8383
File: mcp-gateway-authorization
84-
- Name: Using credentials to access external APIs
85-
File: mcp-gateway-vault
84+
- Name: Revoking MCP server tool access
85+
File: mcp-gateway-revoke-tool-access
86+
#- Name: Using credentials to access external APIs
87+
# File: mcp-gateway-vault
8688
---
8789
Name: Observability and Troubleshooting
8890
Dir: observe_troubleshoot
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
:_mod-docs-content-type: ASSEMBLY
2+
[id="mcp-gateway-revoke-mcp-server-tool-access"]
3+
= Revoking MCP server tool access
4+
include::_attributes/attributes.adoc[]
5+
:context: mcp-gateway-revoke-mcp-server-tool-access
6+
7+
[role="_abstract"]
8+
You can revoke tool access for users and groups. Tool revocation prevents a user or group from calling specific MCP tools.
9+
10+
include::modules/con-mcp-gateway-understand-tool-access-revocation.adoc[leveloffset=+1]
11+
12+
include::modules/proc-mcp-gateway-revoking-tools-call-requests.adoc[leveloffset=+1]
13+
14+
include::modules/proc-mcp-gateway-filter-revoked-tools-display.adoc[leveloffset=+1]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Module included in the following assemblies:
2+
//
3+
// *mcp_gateway_config/mcp-gateway-revoke-tool-access.adoc
4+
5+
:_mod-docs-content-type: CONCEPT
6+
[id="con-mcp-gateway-understand-tool-access-revocation_{context}"]
7+
= Understanding tool access revocation
8+
9+
[role="_abstract"]
10+
Tool revocation uses the authorization setup where tool access is controlled by roles in identity provider JWT tokens. Revoking a tool means removing the corresponding role from a user or group, so their next token no longer grants access.
11+
12+
[TIP]
13+
====
14+
You cannot revoke a specific in-flight token or revoke access instantly. JWT tokens govern access, and these tokens are valid until they expire. Revocation relies on the token expiring and being reissued with changed permissions. To force faster revocation, reduce the access-token lifespan in your identity provider.
15+
====
16+
17+
The following two enforcement points apply:
18+
19+
* `tools/call`: The `AuthPolicy` custom resource (CR) CEL expression checks the `x-mcp-toolname` header against the user's `resource_access` roles. A revoked tool returns a `failed to create session for mcp server: failed to create client: transport error: server returned 4xx for initialize POST, likely a legacy SSE server` error.
20+
* `tools/list`: The broker filters the tools list using the signed `x-authorized-tools` header. A revoked tool is no longer displayed in the list.
21+
22+
The timing of tool revocation depends on the following session events:
23+
24+
* New sessions: Users who authenticate after revocation receive a token without the revoked tool. They are denied immediately.
25+
26+
* Existing sessions: Users with an active token keep access until the token expires. You configure token lifetime in your identity provider, for example, the **Access Token Lifespan** setting in {keycloak}. Shorter token lifetimes mean that tokens are refreshed more frequently, picking up role changes sooner. You must decide on the balance between revocation latency and token-refresh resource utilization.
27+
28+
* In-flight requests: A `tools/call` request that is already being processed completes normally. The authorization check occurs before the request reaches the backend MCP server, so only new requests can be denied.
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// Module included in the following assemblies:
2+
//
3+
// *mcp_gateway_config/mcp-gateway-revoke-tool-access.adoc
4+
5+
:_mod-docs-content-type: PROCEDURE
6+
[id="proc-mcp-gateway-filter-revoked-tools-display_{context}"]
7+
= Filtering revoked tools from tools/list display
8+
9+
[role="_abstract"]
10+
After you revoke a tool by using your identity provider, `tools/call` requests are blocked. However, the revoked tool is still displayed in `tools/list` responses. To filter revoked tools from the displayed list, you must give the MCP broker component a signed header that carries the user's authorized tools.
11+
12+
You can remove blocked tools from responses by configuring the Authorino Operator to generate the header by using a wristband JWT signed with an ECDSA key pair.
13+
14+
.Prerequisites
15+
16+
* You installed {mcpg}.
17+
* You configured a `Gateway` object with both `mcp` and `mcps` listeners.
18+
* You are logged into a running {ocp} cluster with an `admin` role.
19+
* You configured an `HTTPRoute` object for the gateway.
20+
* You registered an MCP server.
21+
* You configured authentication with an `AuthPolicy` CR on the `mcp` listener.
22+
* You configured authorization with a tool-level `AuthPolicy` CR on the `mcps` listener.
23+
24+
.Procedure
25+
26+
. Generate an ECDSA key pair by running the following commands:
27+
28+
.. Generate the private key and save it to a file by running the following command:
29+
+
30+
[source,terminal]
31+
----
32+
$ openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
33+
----
34+
35+
.. Generate the public key and save it to a file by running the following command:
36+
+
37+
[source,terminal]
38+
----
39+
$ openssl ec -in private-key.pem -pubout -out public-key.pem
40+
----
41+
42+
. Create a Kubernetes `Secret` object in the Authorino Operator namespace by adding the private key with the following command:
43+
+
44+
[source,terminal,subs="+quotes"]
45+
----
46+
$ oc create secret generic trusted-headers-private-key \
47+
--from-file=key.pem=private-key.pem \
48+
-n _<kuadrant_system>_ \
49+
--dry-run=client -o yaml | oc apply -f -
50+
----
51+
+
52+
Replace `_<kuadrant_system>_` with the namespace where {prodname} is installed.
53+
54+
. Create a Kubernetes `Secret` object in the MCP broker component namespace by adding the public key with the following command:
55+
+
56+
[source,terminal,subs="+quotes"]
57+
----
58+
$ oc create secret generic trusted-headers-public-key \
59+
--from-file=key=public-key.pem \
60+
-n _<mcp_system>_ \
61+
--dry-run=client -o yaml | oc apply -f -
62+
----
63+
+
64+
Replace `_<mcp_system>_` with the namespace where {mcpg} is installed.
65+
66+
. Delete only the existing `AuthPolicy` CR on the `mcp` listener by running the following command:
67+
+
68+
[source,terminal,subs="+quotes"]
69+
----
70+
$ oc delete authpolicy _<mcp_authn_policy>_ -n _<authn_namespace>_ --ignore-not-found
71+
----
72+
+
73+
* Replace `_<mcp_authn_policy>_` with the name of your `AuthPolicy` CR.
74+
* Replace `_<authn_namespace>_` with the namespace where your `AuthPolicy` CR is applied.
75+
+
76+
[IMPORTANT]
77+
====
78+
You must delete the `AuthPolicy` CR and create a new one. Using `$ oc apply` merges both the new and old policies.
79+
====
80+
81+
. Generate a new `x-authorized-tools` header by creating and applying a new `AuthPolicy` CR. Use the following example:
82+
+
83+
.Example AuthPolicy CR
84+
[source,yaml]
85+
----
86+
$ oc create -f - <<EOF
87+
apiVersion: kuadrant.io/v1
88+
kind: AuthPolicy
89+
metadata:
90+
name: _<mcp_auth_policy>_
91+
namespace: _<authn_namespace>_
92+
spec:
93+
targetRef:
94+
group: gateway.networking.k8s.io
95+
kind: Gateway
96+
name: _<mcp_gateway>_
97+
sectionName: mcp
98+
when:
99+
- predicate: "!request.path.contains('/.well-known')"
100+
rules:
101+
authentication:
102+
'keycloak':
103+
jwt:
104+
issuerUrl: https://keycloak.example.com:port/realms/mcp
105+
authorization:
106+
'allow-mcp-method':
107+
patternMatching:
108+
patterns:
109+
- predicate: |
110+
!request.headers.exists(h, h == 'x-mcp-method') || (request.headers['x-mcp-method'] in ["tools/list","initialize","notifications/initialized"])
111+
'authorized-tools':
112+
opa:
113+
rego: |
114+
allow = true
115+
tools = { server: roles | server := object.keys(input.auth.identity.resource_access)[_]; roles := object.get(input.auth.identity.resource_access, server, {}).roles }
116+
allValues: true
117+
response:
118+
success:
119+
headers:
120+
x-authorized-tools:
121+
wristband:
122+
issuer: 'authorino'
123+
customClaims:
124+
'allowed-tools':
125+
selector: auth.authorization.authorized-tools.tools.@tostr
126+
tokenDuration: 300
127+
signingKeyRefs:
128+
- name: trusted-headers-private-key
129+
algorithm: ES256
130+
unauthenticated:
131+
headers:
132+
'WWW-Authenticate':
133+
value: Bearer resource_metadata=https://mcp.example.com:port/.well-known/oauth-protected-resource
134+
body:
135+
value: |
136+
{
137+
"error": "Unauthorized",
138+
"message": "Access denied: Authentication required."
139+
}
140+
unauthorized:
141+
code: 401
142+
headers:
143+
'WWW-Authenticate':
144+
value: Bearer resource_metadata=https://mcp.example.com:port/.well-known/oauth-protected-resource
145+
body:
146+
value: |
147+
{
148+
"error": "Forbidden",
149+
"message": "Access denied: Unsupported method. New authentication required (401)."
150+
}
151+
EOF
152+
----
153+
+
154+
* Replace `_<mcp_auth_policy>_` with the name of your new `AuthPolicy` CR.
155+
* Replace `_<authn_namespace>_` with the namespace where you removed the `AuthPolicy CR` that was on the `mcp` listener.
156+
* Replace `_<mcp_gateway>_` with the name of your {mcpg} deployment.
157+
158+
. Configure the MCP broker component to validate the signed header by running the following command:
159+
+
160+
[source,terminal,subs="+quotes"]
161+
----
162+
$ oc patch mcpgatewayextension _<mcp_gateway_extension>_ -n _<mcp_system>_ --type='merge' \
163+
-p='{"spec":{"trustedHeadersKey":{"secretName":"trusted-headers-public-key"}}}'
164+
----
165+
+
166+
* Replace `_<mcp_gateway_extension>_` with the name of your `MCPGatewayExtension` CR.
167+
* Replace `_<mcp_system>_` with the namespace where {mcpg} is installed.
168+
169+
. Wait for the automatic MCP broker component redeployment to load the public key from the `Secret` CR created in the earlier step by running the following command:
170+
+
171+
[source,terminal,subs="+quotes"]
172+
----
173+
$ oc rollout status deployment/_<mcp_gateway>_ -n _<mcp_system>_ --timeout=60s
174+
----
175+
+
176+
* Replace `_<mcp_gateway>_` with the name of your {mcpg} deployment.
177+
* Replace `_<mcp_system>_` with the namespace where your {mcpg} deployment is applied.
178+
179+
. If the redeployment does not start, force it by running the following command:
180+
+
181+
[source,terminal,subs="+quotes"]
182+
----
183+
$ oc rollout restart deployment/_<mcp_gateway>_ -n _<mcp_system>_
184+
----
185+
+
186+
* Replace `_<mcp_gateway>_` with the name of your {mcpg} deployment.
187+
* Replace `_<mcp_system>_` with the namespace where your {mcpg} deployment is applied.
188+
189+
.Verification
190+
191+
. Verify that the new `AuthPolicy` CR is enforced by running the following command:
192+
+
193+
[source,terminal,subs="+quotes"]
194+
----
195+
$ oc get authpolicy _<mcp_auth_policy>_ -n _<authn_namespace>_ -o jsonpath='{.status.conditions[?(@.type=="Enforced")].status}'
196+
----
197+
+
198+
* Replace `_<mcp_auth_policy>_` with the name of your new `AuthPolicy` CR.
199+
* Replace `_<authn_namespace>_` with the namespace where your `AuthPolicy` CR is applied.
200+
+
201+
.Example output
202+
[source,text]
203+
----
204+
True
205+
----
206+
207+
. Log out and log back in with the user credentials that you changed access for to get a fresh token.
208+
209+
. Request all available tools from your MCP servers for the user or group whose access you revoked. The expected response is a list of all tools that the user or group can access from all registered MCP servers.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Module included in the following assemblies:
2+
//
3+
// *mcp_gateway_config/mcp-gateway-revoke-tool-access.adoc
4+
5+
:_mod-docs-content-type: PROCEDURE
6+
[id="proc-mcp-gateway-revoking-tool-call-requests_{context}"]
7+
= Revoking tools/call requests
8+
9+
[role="_abstract"]
10+
Use your existing authorization setup to revoke `tools/call` requests for individual users and groups by removing the tool role from the user or group in your identity provider.
11+
12+
In this example, {keycloak} is used. Remove the client-role mapping. The client name corresponds to the namespaced MCPServerRegistration custom resource (CR), such as `mcp-test/test-server1`. Each role represents a tool name, such as `greet`, or `headers`.
13+
14+
.Prerequisites
15+
16+
* You installed {mcpg}.
17+
* You configured a `Gateway` object.
18+
* You are logged into a running {ocp} cluster with an `admin` role.
19+
* You configured an `HTTPRoute` object for the gateway.
20+
* You registered an MCP server.
21+
* You created a `Secret` CR for authentication.
22+
* You configured authorization with a tool-level `AuthPolicy` CR.
23+
24+
.Procedure
25+
26+
. Revoke a tool for a group by using the {keycloak} interface:
27+
28+
.. Go to **Groups** > select the group, such as `accounting`.
29+
.. Go to **Role mapping** > remove the tool role from the relevant client.
30+
31+
. Revoke a tool for a single user by using the interface:
32+
33+
.. Go to **Users** > select the user.
34+
.. Go to **Role mapping** > remove the tool role from the relevant client.
35+
36+
.Verification
37+
38+
. After revoking a tool, verify that the user can no longer call it by using MCP Inspector. Log out of any existing MCP Inspector session and log back in as the affected user to get a fresh token.
39+
40+
.. Open MCP Inspector and connect to your gateway's `/mcp` endpoint. Authenticate through the OAuth flow.
41+
42+
.. Under **Tools > List Tools**, the revoked tool is displayed. Try calling the revoked tool. The request should return `500`.

0 commit comments

Comments
 (0)