@@ -119,20 +119,18 @@ mapping.
119119
120120### Understanding the AWS MCP Server permission model
121121
122- AWS MCP Server authorization works in two layers:
122+ AWS MCP Server authorization works at the AWS service level using standard IAM
123+ policies. There are no separate ` aws-mcp:* ` actions — you grant the AWS service
124+ permissions you want users to have, and the AWS MCP Server enforces them.
123125
124- 1 . ** MCP layer** (` aws-mcp:* ` actions) - controls which categories of MCP tools
125- the user can invoke
126- 2 . ** AWS service layer** (e.g., ` s3:* ` , ` ec2:* ` ) - controls what the
127- ` aws___call_aws ` tool can actually do when it makes AWS API calls
126+ Two IAM condition keys let you scope policies specifically to MCP traffic:
128127
129- The ` aws-mcp ` namespace defines three actions:
130-
131- - ` InvokeMcp ` - required to connect and discover available tools
132- - ` CallReadOnlyTool ` - search documentation, list regions, get CLI suggestions
133- (most tools)
134- - ` CallReadWriteTool ` - execute real AWS API calls via the ` aws___call_aws ` tool
135- (requires additional service permissions)
128+ - ` aws:ViaAWSMCPService ` (Bool) — ` true ` for any request routed through an
129+ AWS-managed MCP server. Use this when you want to allow access via any AWS MCP
130+ server, or to deny direct API access outside of MCP.
131+ - ` aws:CalledViaAWSMCP ` (String) — identifies the specific MCP server that
132+ originated the request (for example, ` aws-mcp.amazonaws.com ` ). Use this when
133+ you want to restrict access to a particular MCP server endpoint.
136134
137135### Default role
138136
@@ -163,17 +161,35 @@ condition rejects tokens meant for other services:
163161}
164162```
165163
166- The permission policy grants read-only MCP access. Users can search AWS
167- documentation and get suggestions, but cannot execute AWS API calls:
164+ The permission policy grants minimal access. It allows ` sts:GetCallerIdentity `
165+ as a smoke test (so you can verify the role assumption works), and includes a
166+ deny guardrail that prevents the credentials from being used outside of MCP:
168167
169168``` json title="default-mcp-permissions.json"
170169{
171170 "Version" : " 2012-10-17" ,
172171 "Statement" : [
173172 {
173+ "Sid" : " AllowMinimalAccessViaMCP" ,
174174 "Effect" : " Allow" ,
175- "Action" : [" aws-mcp:InvokeMcp" , " aws-mcp:CallReadOnlyTool" ],
176- "Resource" : " *"
175+ "Action" : " sts:GetCallerIdentity" ,
176+ "Resource" : " *" ,
177+ "Condition" : {
178+ "Bool" : {
179+ "aws:ViaAWSMCPService" : " true"
180+ }
181+ }
182+ },
183+ {
184+ "Sid" : " DenyDirectAPIAccess" ,
185+ "Effect" : " Deny" ,
186+ "Action" : " *" ,
187+ "Resource" : " *" ,
188+ "Condition" : {
189+ "BoolIfExists" : {
190+ "aws:ViaAWSMCPService" : " false"
191+ }
192+ }
177193 }
178194 ]
179195}
@@ -207,34 +223,42 @@ blast radius even if a role's identity policy is overly permissive.
207223
208224You can create additional roles with different permissions and map them to
209225specific groups using ToolHive's role mappings. This example creates a role that
210- grants ` CallReadWriteTool ` (so the ` aws___call_aws ` tool can execute API calls)
211- and scopes the underlying AWS permissions to S3 read-only access:
226+ grants S3 read-only access when using the AWS MCP Server:
212227
213228``` json title="s3-readonly-permissions.json"
214229{
215230 "Version" : " 2012-10-17" ,
216231 "Statement" : [
217232 {
233+ "Sid" : " AllowS3ReadAccessViaMCP" ,
218234 "Effect" : " Allow" ,
219- "Action" : [
220- " aws-mcp:InvokeMcp" ,
221- " aws-mcp:CallReadOnlyTool" ,
222- " aws-mcp:CallReadWriteTool"
223- ],
224- "Resource" : " *"
235+ "Action" : [" s3:GetObject" , " s3:ListBucket" ],
236+ "Resource" : " *" ,
237+ "Condition" : {
238+ "StringEquals" : {
239+ "aws:CalledViaAWSMCP" : " aws-mcp.amazonaws.com"
240+ }
241+ }
225242 },
226243 {
227- "Effect" : " Allow" ,
228- "Action" : [" s3:GetObject" , " s3:ListBucket" ],
229- "Resource" : " *"
244+ "Sid" : " DenyDirectAPIAccess" ,
245+ "Effect" : " Deny" ,
246+ "Action" : " *" ,
247+ "Resource" : " *" ,
248+ "Condition" : {
249+ "BoolIfExists" : {
250+ "aws:ViaAWSMCPService" : " false"
251+ }
252+ }
230253 }
231254 ]
232255}
233256```
234257
235- The first statement unlocks the ` aws___call_aws ` tool. The second statement
236- limits what that tool can actually do - in this case, only S3 read operations.
237- Without the S3 permissions, API calls to other services would be denied by IAM.
258+ No ` aws-mcp:* ` actions are required — you grant only the AWS service permissions
259+ you need. The ` aws:CalledViaAWSMCP ` condition scopes access to requests
260+ originating from the AWS MCP Server, and the deny guardrail prevents the
261+ temporary credentials from being used outside of MCP.
238262
239263``` bash
240264aws iam create-role \
@@ -263,6 +287,9 @@ spec:
263287 awsSts :
264288 region : <YOUR_AWS_REGION>
265289
290+ # SigV4 service name for the AWS MCP Server
291+ service : aws-mcp
292+
266293 # Default role when no role mapping matches
267294 fallbackRoleArn : >-
268295 arn:aws:iam::<YOUR_AWS_ACCOUNT_ID>:role/DefaultMCPRole
@@ -347,10 +374,13 @@ spec:
347374 # OIDC configuration for validating incoming client tokens
348375 oidcConfig:
349376 type: inline
377+ # Public URL of this proxy's MCP endpoint, advertised to clients
378+ # via WWW-Authenticate so they can discover your OIDC provider.
379+ # Must match the hostname you configure in Step 5.
380+ resourceUrl: https://<YOUR_DOMAIN>/mcp
350381 inline:
351382 issuer: https://<YOUR_OIDC_ISSUER>
352383 audience: <YOUR_OIDC_AUDIENCE>
353- clientId: <YOUR_OIDC_CLIENT_ID>
354384
355385 proxyPort: 8080
356386 transport: streamable-http
@@ -492,11 +522,19 @@ TOKEN=$(oauth2c https://<YOUR_OIDC_ISSUER> \
492522 --response-types code \
493523 --pkce | jq -r '.access_token')
494524
495- # This should return a list of tools
525+ # Step 1: initialize the MCP session and capture the session ID
526+ SESSION=$(curl -si -X POST http://localhost:8080/mcp \
527+ -H "Authorization: Bearer $TOKEN" \
528+ -H "Content-Type: application/json" \
529+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}' \
530+ | grep -i "^mcp-session-id:" | awk '{print $2}' | tr -d '\r ')
531+
532+ # Step 2: list available tools using the session ID
496533curl -X POST http://localhost:8080/mcp \
497534 -H "Authorization: Bearer $TOKEN" \
498535 -H "Content-Type: application/json" \
499- -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
536+ -H "Mcp-Session-Id: $SESSION" \
537+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
500538` ` `
501539
502540Check the proxy logs to confirm role selection is working :
@@ -508,6 +546,58 @@ kubectl logs -n toolhive-system \
508546
509547Look for log entries showing role selection and STS exchange results.
510548
549+ # # Security best practices
550+
551+ # ## The deny guardrail pattern
552+
553+ Both policy examples above include a `DenyDirectAPIAccess` statement that uses
554+ `BoolIfExists` :
555+
556+ ` ` ` json
557+ {
558+ "Sid": "DenyDirectAPIAccess",
559+ "Effect": "Deny",
560+ "Action": "*",
561+ "Resource": "*",
562+ "Condition": {
563+ "BoolIfExists": {
564+ "aws:ViaAWSMCPService": "false"
565+ }
566+ }
567+ }
568+ ` ` `
569+
570+ ` BoolIfExists` is important here. Without the `IfExists` suffix, the deny would
571+ apply even when the condition key is absent — which would block legitimate STS
572+ operations like `AssumeRoleWithWebIdentity` itself. With `IfExists`, the
573+ condition only evaluates when the key is present :
574+
575+ | `aws:ViaAWSMCPService` present? | Value | Deny applies? |
576+ | ------------------------------- | ------- | ------------- |
577+ | No (key absent) | — | No |
578+ | Yes | `true` | No |
579+ | Yes | `false` | Yes |
580+
581+ This means credentials can only be used when the request flows through an AWS
582+ MCP server. If the temporary credentials are ever extracted and used directly
583+ against the AWS API, the deny fires and access is blocked.
584+
585+ # ## Choosing between condition keys
586+
587+ Use `aws:ViaAWSMCPService` when you want to :
588+
589+ - Allow or deny access based on whether the request came through _any_
590+ AWS-managed MCP server
591+ - Write deny guardrails that apply regardless of which MCP server is used
592+
593+ Use `aws:CalledViaAWSMCP` when you want to :
594+
595+ - Scope access to a _specific_ MCP server endpoint (for example,
596+ ` aws-mcp.amazonaws.com` )
597+ - Prevent credentials from being used with other MCP servers
598+
599+ You can combine both in a single policy for the tightest scoping.
600+
511601# # Observability and audit
512602
513603ToolHive sets the STS session name to the user's `sub` claim from their JWT.
@@ -550,7 +640,7 @@ aws iam delete-open-id-connect-provider \
550640 arn:aws:iam::<YOUR_AWS_ACCOUNT_ID>:oidc-provider/<YOUR_OIDC_ISSUER>
551641` ` `
552642
553- # # What's next?
643+ # # Next steps
554644
555645- Learn about the concepts behind
556646 [backend authentication](../concepts/backend-auth.mdx) and
@@ -589,19 +679,22 @@ aws iam get-role --role-name DefaultMCPRole \
589679
590680<details>
591681<summary>Service access denied : " User is not authorized to perform
592- aws-mcp:InvokeMcp " </summary>
682+ s3:ListBucket " (or similar) </summary>
593683
594- This error means the assumed role doesn't have the required permissions. Verify
595- the permission policy attached to the role includes the necessary actions :
684+ This error means the assumed role doesn't have the required AWS service
685+ permissions. Authorization is now at the service level — there are no
686+ ` aws-mcp:*` actions. Verify the permission policy attached to the role includes
687+ the actions the MCP tool needs :
596688
597689` ` ` bash
598690aws iam get-role-policy \
599- --role-name DefaultMCPRole \
600- --policy-name DefaultMCPPolicy
691+ --role-name S3ReadOnlyMCPRole \
692+ --policy-name S3ReadOnlyMCPPolicy
601693` ` `
602694
603- Ensure the policy includes `aws-mcp:InvokeMcp` and any other actions your MCP
604- tools require.
695+ Ensure the policy includes the relevant service actions (for example,
696+ ` s3:GetObject` , `s3:ListBucket`) and that the `aws:CalledViaAWSMCP` or
697+ ` aws:ViaAWSMCPService` conditions are correctly set.
605698
606699</details>
607700
0 commit comments