Skip to content

Commit e4d2ede

Browse files
jhrozekclaude
andauthored
Update AWS STS tutorial for new authorization model (#676)
* Update AWS STS tutorial for new authorization model AWS MCP Server no longer uses aws-mcp:* IAM actions. Authorization now happens at the AWS service level using two new condition keys: aws:ViaAWSMCPService and aws:CalledViaAWSMCP. - Replace aws-mcp:* permission model explanation with new single-layer model and document both condition keys - Update default role policy to use sts:GetCallerIdentity scoped to aws:ViaAWSMCPService with a BoolIfExists deny guardrail - Update S3 role policy to use service-level actions with aws:CalledViaAWSMCP condition - Add security best practices section covering the deny guardrail pattern, BoolIfExists truth table, and condition key guidance - Update troubleshooting section to reflect service-level errors - Add missing service: aws-mcp field to MCPExternalAuthConfig example - Add resourceUrl to MCPRemoteProxy oidcConfig example for OAuth protected resource discovery Depends on stacklok/toolhive#4670 (SigV4 proxy header fix). Fixes #587 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Address editorial review of AWS STS tutorial Four issues found during docs review: - proxyPort: the MCPRemoteProxy example used the deprecated `port` field. CRD spec marks it as deprecated in favor of `proxyPort`. - Verification test: the curl test called tools/list directly, which requires a prior initialize handshake. Added the two-step flow: initialize (capturing Mcp-Session-Id), then tools/list with the session ID header. - resourceUrl cross-reference: the oidcConfig example uses <YOUR_DOMAIN> before the reader has chosen a domain (Step 5 comes later). Added a comment making the dependency explicit. - Section title: renamed "What's next?" to "Next steps" to match the project style guide. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 45360b1 commit e4d2ede

File tree

1 file changed

+134
-41
lines changed

1 file changed

+134
-41
lines changed

docs/toolhive/integrations/aws-sts.mdx

Lines changed: 134 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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

208224
You can create additional roles with different permissions and map them to
209225
specific 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
240264
aws 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
496533
curl -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

502540
Check the proxy logs to confirm role selection is working:
@@ -508,6 +546,58 @@ kubectl logs -n toolhive-system \
508546

509547
Look 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

513603
ToolHive 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
598690
aws 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

Comments
 (0)