Update AWS STS tutorial for new authorization model#676
Update AWS STS tutorial for new authorization model#676
Conversation
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>
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Updates the ToolHive AWS STS integration documentation to reflect the AWS MCP Server authorization model change (service-level IAM permissions scoped via new IAM condition keys), and refreshes the examples to match current CRD fields and MCP client handshake behavior.
Changes:
- Replaces the deprecated
aws-mcp:*action model with service-level IAM examples usingaws:ViaAWSMCPService/aws:CalledViaAWSMCP. - Updates IAM policy examples (default + S3 read-only) and adds a new “Security best practices” section describing the deny-guardrail approach.
- Fixes/extends configuration and verification examples (adds
awsSts.service, addsoidcConfig.resourceUrl, and updates the curl flow to includeinitializebeforetools/list).
| `BoolIfExists` is important here. Without the `IfExists` suffix, the deny would | ||
| apply even when the condition key is absent — which would block legitimate STS | ||
| operations like `AssumeRoleWithWebIdentity` itself. With `IfExists`, the | ||
| condition only evaluates when the key is present: | ||
|
|
||
| | `aws:ViaAWSMCPService` present? | Value | Deny applies? | | ||
| | ------------------------------- | ------- | ------------- | | ||
| | No (key absent) | — | No | | ||
| | Yes | `true` | No | | ||
| | Yes | `false` | Yes | | ||
|
|
||
| This means credentials can only be used when the request flows through an AWS | ||
| MCP server. If the temporary credentials are ever extracted and used directly | ||
| against the AWS API, the deny fires and access is blocked. |
There was a problem hiding this comment.
The explanation of BoolIfExists here appears inaccurate for IAM condition evaluation. ...IfExists operators generally match when the key is missing, and the role’s identity policy is not evaluated for the initial sts:AssumeRoleWithWebIdentity call (only the trust policy is). Please re-check and adjust the narrative + truth table so it reflects how the deny actually behaves when aws:ViaAWSMCPService is absent vs present, and why that matters for STS exchange vs subsequent API calls made with the temporary credentials.
| `BoolIfExists` is important here. Without the `IfExists` suffix, the deny would | |
| apply even when the condition key is absent — which would block legitimate STS | |
| operations like `AssumeRoleWithWebIdentity` itself. With `IfExists`, the | |
| condition only evaluates when the key is present: | |
| | `aws:ViaAWSMCPService` present? | Value | Deny applies? | | |
| | ------------------------------- | ------- | ------------- | | |
| | No (key absent) | — | No | | |
| | Yes | `true` | No | | |
| | Yes | `false` | Yes | | |
| This means credentials can only be used when the request flows through an AWS | |
| MCP server. If the temporary credentials are ever extracted and used directly | |
| against the AWS API, the deny fires and access is blocked. | |
| `BoolIfExists` is important here, but its behavior is easy to misread. For IAM | |
| condition operators with the `IfExists` suffix, the condition matches when the | |
| key is absent, and otherwise evaluates the operator normally. For this deny | |
| statement, that means the deny applies when `aws:ViaAWSMCPService` is missing or | |
| explicitly `false`, and does not apply only when the key is present and `true`: | |
| | `aws:ViaAWSMCPService` present? | Value | Deny applies? | | |
| | ------------------------------- | ------- | ------------- | | |
| | No (key absent) | — | Yes | | |
| | Yes | `true` | No | | |
| | Yes | `false` | Yes | | |
| That does **not** block the initial | |
| [`AssumeRoleWithWebIdentity`](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html) | |
| exchange. When AWS evaluates that request, it uses the role's **trust policy**; | |
| the role's identity policy is not evaluated yet. This deny statement matters | |
| for **subsequent AWS API calls made with the temporary credentials**. If those | |
| credentials are used directly against AWS APIs, `aws:ViaAWSMCPService` is | |
| typically absent and the deny blocks the request. When the request is routed | |
| through an AWS MCP server, the key is present and `true`, so this deny does not | |
| apply. |
There was a problem hiding this comment.
Fixed in 2d67adf — corrected the truth table (key absent → deny applies: Yes) and rewrote the prose to clarify that BoolIfExists matches when the key is absent, and that this deny applies to API calls made with the temporary credentials, not to the AssumeRoleWithWebIdentity exchange itself (which is governed by the trust policy).
Also went further in e0e7bb9 after verifying with AWS docs: sts:GetCallerIdentity is immune to all IAM evaluation (it always succeeds and cannot be denied), so the previous AllowMinimalAccessViaMCP statement was inert and the deny guardrail had no effect on it. Replaced the default policy with a deny-only statement — the role is now correctly documented as a no-access fallback whose only purpose is letting the OIDC token exchange succeed and blocking misuse of the resulting credentials.
| type: inline | ||
| # Public URL of this proxy's MCP endpoint, advertised to clients | ||
| # via WWW-Authenticate so they can discover your OIDC provider. | ||
| # Must match the hostname you configure in Step 5. |
There was a problem hiding this comment.
The inline comment says resourceUrl must match the hostname configured in Step 5, but resourceUrl is intended to be the full client-facing resource URL (scheme + host + path) used for protected resource/OIDC discovery. Consider rewording to avoid readers setting only the hostname and omitting the /mcp path (see similar guidance in docs/toolhive/guides-k8s/connect-clients.mdx:234-245).
| # Must match the hostname you configure in Step 5. | |
| # Must match the full public MCP endpoint URL you configure in Step 5, | |
| # including the scheme, host, and /mcp path. |
There was a problem hiding this comment.
Fixed in 2d67adf — updated the comment to say "full public MCP endpoint URL you configure in Step 5, including the scheme, host, and /mcp path" to make clear the entire URL is required, not just the hostname.
Description
AWS MCP Server no longer uses
aws-mcp:*IAM actions (effectiveMarch 24, 2026). Authorization now happens at the AWS service level
using two new IAM condition keys:
aws:ViaAWSMCPServiceandaws:CalledViaAWSMCP.Changes to
docs/toolhive/integrations/aws-sts.mdx:aws-mcp:*permission model explanation withthe new single-layer model, documenting both condition keys with
descriptions and when to use each
sts:GetCallerIdentityscoped to
aws:ViaAWSMCPService, with aBoolIfExistsdeny guardrailthat prevents the temporary credentials from being used outside of MCP
(
s3:GetObject,s3:ListBucket) withaws:CalledViaAWSMCPcondition;remove
aws-mcp:*statement entirelypattern,
BoolIfExiststruth table, and guidance on choosing betweenthe two condition keys
instead of
aws-mcp:InvokeMcpservice: aws-mcpfield to theMCPExternalAuthConfigexample (required for correct SigV4 signing)
resourceUrlto theMCPRemoteProxyoidcConfigexample(required for OAuth 2.0 protected resource discovery, enabling
MCP clients to auto-discover the OIDC provider)
tools/listrequires a priorinitializehandshake; updated to a two-step curl flow
port→proxyPort(theportfield is deprecated per CRD spec)Depends on stacklok/toolhive#4670 (SigV4 proxy header fix required
for requests routed through Gateway API / ngrok).
Type of change
Related issues/PRs
Fixes #587
Depends on stacklok/toolhive#4670
Submitter checklist
Content and formatting