@@ -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
9597Replace 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
261252Create an ` MCPExternalAuthConfig ` resource that defines how ToolHive exchanges
262253OIDC 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"
265256apiVersion : toolhive.stacklok.dev/v1alpha1
266257kind : MCPExternalAuthConfig
267258metadata :
@@ -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+
292295Apply 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
306321roleMappings:
@@ -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
358372Apply the proxy :
359373
@@ -375,7 +389,11 @@ When you apply this resource, the ToolHive Operator:
375389# # Step 5: Expose the proxy
376390
377391To 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"
381399apiVersion: gateway.networking.k8s.io/v1
@@ -424,8 +442,9 @@ configuration.
424442kubectl 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
460479Now 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
466485TOKEN=$(oauth2c https://<YOUR_OIDC_ISSUER> \
@@ -514,7 +533,7 @@ kubectl delete gateway aws-mcp-gateway -n toolhive-system
514533kubectl 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
520539aws iam delete-role-policy \
@@ -528,7 +547,7 @@ aws iam delete-role --role-name S3ReadOnlyMCPRole
528547
529548aws 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