Skip to content

Commit 67e46d5

Browse files
committed
Address PR review feedback on AWS STS tutorial
- Fix OIDC issuer placeholder: rename <YOUR_OIDC_ISSUER_HOST> to <YOUR_OIDC_ISSUER>, clarify it excludes the https:// scheme and must include path components (avoids doubled-scheme copy-paste error and works for Okta/Keycloak issuers with paths) - Clarify roleClaim vs claim: explain that roleClaim defaults to "groups" when omitted, that the claim field under roleMappings is a value to match (not a claim name), and show how to override the claim name for other IdPs - Remove misleading region suggestion: the AWS MCP Server endpoint only exists in us-east-1 - Add Gateway API note and connect-clients cross-reference in Step 5 - Move role selection admonition from Step 2 to Step 3 where the priority field first appears - Add AWS CLI to prerequisites - Replace stale "7 of 9 tools" with "most tools" - Mention jq dependency alongside oauth2c at point of use - Simplify line highlighting to {4,7} - Remove "Optionally" from IAM cleanup
1 parent 8afe588 commit 67e46d5

1 file changed

Lines changed: 53 additions & 34 deletions

File tree

docs/toolhive/tutorials/aws-sts-integration.mdx

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ Before starting this tutorial, ensure you have:
1818
- A Kubernetes cluster with the ToolHive Operator installed (see the
1919
[Kubernetes quickstart guide](./quickstart-k8s.mdx))
2020
- `kubectl` configured to access your cluster
21-
- An AWS account with permissions to create IAM roles, policies, and OIDC
22-
identity providers
21+
- The
22+
[AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
23+
installed and configured with an AWS account that has permissions to create
24+
IAM roles, policies, and OIDC identity providers
2325
- An OIDC identity provider (such as Okta, Auth0, Microsoft Entra ID, or
2426
Keycloak) that your users authenticate with
2527
- Basic familiarity with AWS IAM concepts and OIDC
@@ -94,8 +96,9 @@ aws iam create-open-id-connect-provider \
9496

9597
Replace the placeholders:
9698

97-
- `<YOUR_OIDC_ISSUER>` - your identity provider's issuer URL (for example,
98-
`https://dev-123456.okta.com/oauth2/default`)
99+
- `<YOUR_OIDC_ISSUER>` - your identity provider's issuer identifier without the
100+
`https://` scheme. Include any path components (for example,
101+
`dev-123456.okta.com/oauth2/default`)
99102
- `<YOUR_OIDC_AUDIENCE>` - the audience claim in your OIDC tokens that
100103
identifies this proxy (for example, `toolhive-aws-proxy`)
101104

@@ -127,7 +130,7 @@ The `aws-mcp` namespace defines three actions:
127130

128131
- `InvokeMcp` - required to connect and discover available tools
129132
- `CallReadOnlyTool` - search documentation, list regions, get CLI suggestions
130-
(7 of 9 tools)
133+
(most tools)
131134
- `CallReadWriteTool` - execute real AWS API calls via the `aws___call_aws` tool
132135
(requires additional service permissions)
133136

@@ -147,12 +150,12 @@ condition rejects tokens meant for other services:
147150
{
148151
"Effect": "Allow",
149152
"Principal": {
150-
"Federated": "arn:aws:iam::<YOUR_AWS_ACCOUNT_ID>:oidc-provider/<YOUR_OIDC_ISSUER_HOST>"
153+
"Federated": "arn:aws:iam::<YOUR_AWS_ACCOUNT_ID>:oidc-provider/<YOUR_OIDC_ISSUER>"
151154
},
152155
"Action": "sts:AssumeRoleWithWebIdentity",
153156
"Condition": {
154157
"StringEquals": {
155-
"<YOUR_OIDC_ISSUER_HOST>:aud": "<YOUR_OIDC_AUDIENCE>"
158+
"<YOUR_OIDC_ISSUER>:aud": "<YOUR_OIDC_AUDIENCE>"
156159
}
157160
}
158161
}
@@ -244,24 +247,12 @@ aws iam put-role-policy \
244247
--policy-document file://s3-readonly-permissions.json
245248
```
246249

247-
:::info[How role selection works]
248-
249-
When a request arrives, ToolHive evaluates your role mappings in priority order
250-
(lower number = higher priority). The first matching rule determines which IAM
251-
role to assume. If no mapping matches, the fallback role is used.
252-
253-
For example, if a user belongs to both `s3-readers` and `developers` groups, and
254-
`s3-readers` has a lower priority number, ToolHive selects the S3 read-only
255-
role.
256-
257-
:::
258-
259250
## Step 3: Create the MCPExternalAuthConfig
260251

261252
Create an `MCPExternalAuthConfig` resource that defines how ToolHive exchanges
262253
OIDC tokens for AWS credentials.
263254

264-
```yaml {4,7,10,13-17} title="aws-sts-auth-config.yaml"
255+
```yaml {4,7} title="aws-sts-auth-config.yaml"
265256
apiVersion: toolhive.stacklok.dev/v1alpha1
266257
kind: MCPExternalAuthConfig
267258
metadata:
@@ -289,6 +280,18 @@ Replace the placeholders:
289280
- `<YOUR_AWS_REGION>` - the AWS region (for example, `us-east-1`)
290281
- `<YOUR_AWS_ACCOUNT_ID>` - your 12-digit AWS account ID
291282

283+
:::info[How role selection works]
284+
285+
When a request arrives, ToolHive evaluates your role mappings in priority order
286+
(lower number = higher priority). The first matching rule determines which IAM
287+
role to assume. If no mapping matches, the fallback role is used.
288+
289+
For example, if a user belongs to both `s3-readers` and `developers` groups, and
290+
`s3-readers` has a lower priority number, ToolHive selects the S3 read-only
291+
role.
292+
293+
:::
294+
292295
Apply the configuration:
293296

294297
```bash
@@ -297,10 +300,22 @@ kubectl apply -f aws-sts-auth-config.yaml
297300

298301
:::info[What's happening?]
299302

300-
The `roleMappings` field uses simple claim matching by default: if the value in
301-
the `roleClaim` JWT claim (here, `groups`) contains `s3-readers`, ToolHive
302-
assumes the S3 read-only role. For more complex matching logic, you can use CEL
303-
expressions in the `matcher` field instead of `claim`:
303+
ToolHive checks the `groups` claim in the JWT by default (controlled by the
304+
`roleClaim` field, which defaults to `"groups"` when omitted). Each mapping's
305+
`claim` field is the **value** to match against that claim. In this example, if
306+
the user's JWT contains `"s3-readers"` in their `groups` array, ToolHive assumes
307+
the S3 read-only role.
308+
309+
If your identity provider uses a different claim name (e.g., `roles` or
310+
`memberOf`), add the `roleClaim` field:
311+
312+
```yaml
313+
awsSts:
314+
roleClaim: roles # look at the "roles" claim instead of "groups"
315+
```
316+
317+
For more complex matching logic, use CEL expressions in the `matcher` field
318+
instead of `claim`:
304319

305320
```yaml
306321
roleMappings:
@@ -352,8 +367,7 @@ spec:
352367
memory: 128Mi
353368
```
354369

355-
Replace the placeholders with your OIDC provider's configuration. Update the
356-
region in `remoteURL` if you're not using `us-east-1`.
370+
Replace the placeholders with your OIDC provider's configuration.
357371

358372
Apply the proxy:
359373

@@ -375,7 +389,11 @@ When you apply this resource, the ToolHive Operator:
375389
## Step 5: Expose the proxy
376390

377391
To make the proxy accessible to clients outside the cluster, create Gateway and
378-
HTTPRoute resources:
392+
HTTPRoute resources. This example uses Kubernetes
393+
[Gateway API](https://gateway-api.sigs.k8s.io/); if your cluster uses a
394+
traditional Ingress controller, see
395+
[Connect clients to MCP servers](../guides-k8s/connect-clients.mdx) for
396+
alternatives.
379397

380398
```yaml title="aws-mcp-gateway.yaml"
381399
apiVersion: gateway.networking.k8s.io/v1
@@ -424,8 +442,9 @@ configuration.
424442
kubectl apply -f aws-mcp-gateway.yaml
425443
```
426444

427-
For detailed guidance on setting up ingress, including ngrok for development,
428-
see
445+
For more on exposing MCP servers, see
446+
[Connect clients to MCP servers](../guides-k8s/connect-clients.mdx). For a
447+
worked example using ngrok for development, see
429448
[Configure secure ingress for MCP servers on Kubernetes](./k8s-ingress-ngrok.mdx).
430449

431450
## Step 6: Verify the integration
@@ -458,9 +477,9 @@ curl -s -o /dev/null -w "%{http_code}" \
458477
```
459478

460479
Now test with a valid OIDC token. This example uses
461-
[oauth2c](https://github.com/cloudentity/oauth2c) to obtain a token via the
462-
authorization code flow, but any method that produces a valid access token from
463-
your identity provider will work:
480+
[oauth2c](https://github.com/cloudentity/oauth2c) and
481+
[jq](https://jqlang.github.io/jq/) to obtain and extract a token, but any method
482+
that produces a valid access token from your identity provider will work:
464483

465484
```bash
466485
TOKEN=$(oauth2c https://<YOUR_OIDC_ISSUER> \
@@ -514,7 +533,7 @@ kubectl delete gateway aws-mcp-gateway -n toolhive-system
514533
kubectl delete httproute aws-mcp-route -n toolhive-system
515534
```
516535

517-
Optionally, remove the AWS IAM resources:
536+
Remove the AWS IAM resources:
518537

519538
```bash
520539
aws iam delete-role-policy \
@@ -528,7 +547,7 @@ aws iam delete-role --role-name S3ReadOnlyMCPRole
528547
529548
aws iam delete-open-id-connect-provider \
530549
--open-id-connect-provider-arn \
531-
arn:aws:iam::<YOUR_AWS_ACCOUNT_ID>:oidc-provider/<YOUR_OIDC_ISSUER_HOST>
550+
arn:aws:iam::<YOUR_AWS_ACCOUNT_ID>:oidc-provider/<YOUR_OIDC_ISSUER>
532551
```
533552

534553
## What's next?

0 commit comments

Comments
 (0)