For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
Google Cloud
Exchange the incoming token for a Google Cloud access token at the Google Security Token Service, with Workload Identity Federation.
Exchange the token that a client sends to the gateway for a Google Cloud access token at the Google Security Token Service (STS), so that Google Cloud APIs authorize each request as the end user.
About
The Google Cloud Security Token Service (STS) implements the RFC 8693 token exchange grant for Workload Identity Federation. You register your identity provider (IdP) as a provider in a workload identity pool. The STS then accepts a JWT that your IdP issued as the subject_token, and returns a short-lived Google Cloud access token for the federated identity in the token’s sub claim.
In this guide, agentgateway performs that exchange for each request to a Google Cloud API. The client sends its usual IdP token, and the Google API receives a Google access token. Google Cloud authorizes the request as the end user’s federated identity, so IAM policies and Cloud Audit Logs reflect the user, not the gateway. No Google service account key is stored in the gateway or the client.
sequenceDiagram
participant Client
participant AGW as agentgateway
participant STS as Google STS
participant API as Google Cloud Storage
Client->>AGW: Request + IdP JWT
AGW->>STS: POST /v1/token<br/>subject_token=IdP JWT<br/>audience=//iam.googleapis.com/.../providers/PROVIDER
STS-->>AGW: Federated Google access token
AGW->>API: Request + Authorization: Bearer Google token
API-->>AGW: Response, authorized for the federated user
AGW-->>Client: Response
Google Cloud trusts the token because of the workload identity pool provider, not because of the caller. The STS token endpoint does not take client authentication, so the exchange has no client ID or client secret to store.
This exchange differs from Google Cloud backend authentication, where the gateway calls Google Cloud with its own identity, such as a service account. Use token exchange when Google Cloud must authorize each request as the user who sent it.
Google STS settings
The Google STS uses the standard RFC 8693 grant, so you configure it with the generic oauthTokenExchange method. The following settings are specific to Google.
| Field | Value for Google |
|---|---|
url | https://sts.googleapis.com/v1/token. Because the scheme is https, the gateway originates TLS to the STS. |
grantType | TokenExchange, the default. |
subjectToken.tokenType | Jwt or IdToken for an OIDC provider. Google does not accept the default, AccessToken, for an OIDC provider. |
requestedTokenType | AccessToken. Required by Google. |
audiences | The full resource name of the workload identity pool provider, in the form //iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID. Note the leading // and the project number. |
scopes | Required by Google. Use https://www.googleapis.com/auth/cloud-platform, or a narrower OAuth scope for the API that you call. |
clientAuth | Omit. The STS does not authenticate the client. |
Before you begin
Follow the Get started guide to install agentgateway.
Follow the Sample app guide to create a gateway proxy with an HTTP listener and deploy the httpbin sample app.
Get the external address of the gateway and save it in an environment variable.
Tip
Kind cluster? Kind does not support
LoadBalancerservices by default. To use this option with a Kind cluster, install and runcloud-provider-kind.export INGRESS_GW_ADDRESS=$(kubectl get svc -n agentgateway-system agentgateway-proxy -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}") echo $INGRESS_GW_ADDRESS
Install the
gcloudCLI and log in to a Google Cloud project where you can create workload identity pools and Cloud Storage buckets, such as with theroles/iam.workloadIdentityPoolAdminandroles/storage.adminroles.Install
jq.
Step 1: Deploy a sample identity provider
Deploy Keycloak as the identity provider that issues the incoming token. The realm in this example has a confidential client, agent, that adds the audience agentgateway to its tokens, and a user, testuser, with the password testpass.
Google requires the issuer to be an https:// URL. Keycloak in this example runs on plain HTTP inside the cluster, so the KC_HOSTNAME variable pins the issuer to https://keycloak.example.com. Google never contacts this host, because you upload the realm’s signing keys to Google instead. In production, use your own IdP and its real issuer URL.
Save the realm definition to a file and load it into a ConfigMap.
cat > agentgateway-realm.json <<'EOF' { "realm": "agentgateway", "enabled": true, "clients": [ { "clientId": "agent", "secret": "agent-secret", "publicClient": false, "standardFlowEnabled": false, "directAccessGrantsEnabled": true, "protocolMappers": [ { "name": "google-audience", "protocol": "openid-connect", "protocolMapper": "oidc-audience-mapper", "config": { "included.custom.audience": "agentgateway", "access.token.claim": "true" } } ] } ], "users": [ { "username": "testuser", "enabled": true, "email": "[email protected]", "emailVerified": true, "firstName": "Test", "lastName": "User", "credentials": [{"type": "password", "value": "testpass", "temporary": false}] } ] } EOF kubectl create configmap keycloak-realm -n agentgateway-system \ --from-file=agentgateway-realm.jsonDeploy Keycloak and its Service.
kubectl apply -f- <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: keycloak namespace: agentgateway-system spec: replicas: 1 selector: matchLabels: app: keycloak template: metadata: labels: app: keycloak spec: containers: - name: keycloak image: quay.io/keycloak/keycloak:26.7.1 args: ["start-dev", "--import-realm", "--http-port=8080"] env: - name: KC_BOOTSTRAP_ADMIN_USERNAME value: admin - name: KC_BOOTSTRAP_ADMIN_PASSWORD value: admin - name: KC_HOSTNAME value: https://keycloak.example.com ports: - containerPort: 8080 volumeMounts: - name: realm mountPath: /opt/keycloak/data/import readOnly: true volumes: - name: realm configMap: name: keycloak-realm --- apiVersion: v1 kind: Service metadata: name: keycloak namespace: agentgateway-system spec: selector: app: keycloak ports: - name: http port: 8080 targetPort: 8080 EOFWait for Keycloak to be ready.
kubectl rollout status deployment/keycloak -n agentgateway-system --timeout=180sPort-forward the Keycloak Service so that you can reach it locally.
kubectl port-forward -n agentgateway-system svc/keycloak 8081:8080In another terminal, mint a token for
testuserand save it in an environment variable. Keycloak tokens expire after 5 minutes, so mint a new one if you come back to this guide later.export INBOUND_TOKEN=$(curl -s http://localhost:8081/realms/agentgateway/protocol/openid-connect/token \ -u agent:agent-secret -d grant_type=password \ -d username=testuser -d password=testpass | jq -r .access_token)Decode the token’s payload and save its
subclaim. Google uses this claim as the identity of the federated user.echo "$INBOUND_TOKEN" | cut -d. -f2 | jq -R 'gsub("-";"+") | gsub("_";"/") | . + ("=" * ((4 - (length % 4)) % 4)) | @base64d | fromjson | {iss, aud, sub}' export USER_SUB=$(echo "$INBOUND_TOKEN" | cut -d. -f2 | jq -rR 'gsub("-";"+") | gsub("_";"/") | . + ("=" * ((4 - (length % 4)) % 4)) | @base64d | fromjson | .sub')Example output:
{ "iss": "https://keycloak.example.com/realms/agentgateway", "aud": "agentgateway", "sub": "c44ff2c5-1833-4e88-ad28-780f41d48e47" }Save the realm’s signing keys to a file, to upload to Google in the next step. The
jqfilter keeps the signing keys and drops the encryption key.curl -s http://localhost:8081/realms/agentgateway/protocol/openid-connect/certs \ | jq '{keys: [.keys[] | select(.use == "sig")]}' > jwks.jsonKeycloak in this example keeps its keys in memory. If the Keycloak pod restarts, it generates new keys, and you must upload the new
jwks.jsonfile to the provider withgcloud iam workload-identity-pools providers update-oidc.
Step 2: Set up Workload Identity Federation
Register Keycloak as an identity provider in a Google Cloud workload identity pool, and give its user access to a Cloud Storage bucket.
Set environment variables for your Google Cloud project and the resources that you create. Bucket names are globally unique, so the bucket name includes your project ID.
export PROJECT_ID=<your-project-id> export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)") export POOL_ID=agentgateway-pool export PROVIDER_ID=keycloak export BUCKET=agentgateway-te-$PROJECT_IDEnable the APIs that Workload Identity Federation and Cloud Storage use.
gcloud services enable iam.googleapis.com sts.googleapis.com storage.googleapis.com \ --project $PROJECT_IDCreate a workload identity pool.
gcloud iam workload-identity-pools create $POOL_ID \ --project $PROJECT_ID \ --location global \ --display-name "agentgateway token exchange"Create an OIDC provider in the pool for the Keycloak realm.
gcloud iam workload-identity-pools providers create-oidc $PROVIDER_ID \ --project $PROJECT_ID \ --location global \ --workload-identity-pool $POOL_ID \ --issuer-uri "https://keycloak.example.com/realms/agentgateway" \ --allowed-audiences agentgateway \ --attribute-mapping "google.subject=assertion.sub" \ --jwk-json-path jwks.jsonSetting Description --issuer-uriMust match the issclaim of the incoming token exactly. Google requires anhttps://issuer.--allowed-audiencesThe audvalues that Google accepts in the incoming token. The sample realm setsaudtoagentgateway. If you omit this flag, the token’saudmust be the full provider resource name,https://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID.--attribute-mappingMaps the token’s subclaim to the Google subject. IAM policies refer to the federated user by this value.--jwk-json-pathUploads the signing keys that you saved earlier, so Google validates tokens without contacting the issuer. For an IdP with a public https://discovery endpoint, omit this flag and Google fetches the keys itself.Create a bucket with a sample object.
gcloud storage buckets create gs://$BUCKET --project $PROJECT_ID --location us-central1 echo "hello from agentgateway" > hello.txt gcloud storage cp hello.txt gs://$BUCKET/hello.txtGrant the Keycloak user read access to the bucket. The member is the federated identity of
testuser: the workload identity pool plus thesubclaim that you saved earlier. Use the project number, not the project ID.gcloud storage buckets add-iam-policy-binding gs://$BUCKET \ --member "principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/subject/$USER_SUB" \ --role roles/storage.objectViewerIAM changes can take a few minutes to take effect.
Step 3: Route to Google Cloud Storage
Create an AgentgatewayBackend for the Cloud Storage API. The
tlspolicy originates TLS to the backend.kubectl apply -f- <<EOF apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayBackend metadata: name: google-storage namespace: agentgateway-system spec: static: host: storage.googleapis.com port: 443 policies: tls: sni: storage.googleapis.com EOFCreate an HTTPRoute that sends requests for the
storage.example.comhost to the backend, and rewrites the host tostorage.googleapis.com.kubectl apply -f- <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: google-storage namespace: agentgateway-system spec: parentRefs: - name: agentgateway-proxy namespace: agentgateway-system hostnames: - storage.example.com rules: - backendRefs: - group: agentgateway.dev kind: AgentgatewayBackend name: google-storage filters: - type: URLRewrite urlRewrite: hostname: storage.googleapis.com EOFList the objects in the bucket through the gateway, with the Keycloak token. Without a token exchange, the gateway forwards the Keycloak token unchanged, and Cloud Storage rejects it with a
401response, because it is not a Google credential.curl -s http://$INGRESS_GW_ADDRESS/storage/v1/b/$BUCKET/o \ -H "host: storage.example.com" \ -H "authorization: Bearer $INBOUND_TOKEN"Example output:
{ "error": { "code": 401, "message": "Invalid Credentials", "errors": [ { "message": "Invalid Credentials", "domain": "global", "reason": "authError", "locationType": "header", "location": "Authorization" } ] } }
Step 4: Configure token exchange
Create an AgentgatewayPolicy that exchanges the incoming token at the Google STS for every request to the Cloud Storage backend.
kubectl apply -f- <<EOF apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayPolicy metadata: name: google-token-exchange namespace: agentgateway-system spec: targetRefs: - group: agentgateway.dev kind: AgentgatewayBackend name: google-storage backend: auth: oauthTokenExchange: url: https://sts.googleapis.com/v1/token grantType: TokenExchange subjectToken: tokenType: Jwt requestedTokenType: AccessToken audiences: - //iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID scopes: - https://www.googleapis.com/auth/cloud-platform EOFFor a description of each setting, see Google STS settings.
Confirm that the policy is accepted and attached.
kubectl get AgentgatewayPolicy google-token-exchange -n agentgateway-system \ -o jsonpath='{range .status.ancestors[0].conditions[*]}{.type}={.status}{"\n"}{end}'Example output:
Accepted=True Attached=True
Step 5: Verify the exchange
Mint a new token for
testuser. The token from Step 1 has likely expired, because Keycloak tokens expire after 5 minutes. Thesubclaim of the new token is the same, so the IAM binding from Step 2 still applies. The Keycloak port-forward from Step 1 must still be running.export INBOUND_TOKEN=$(curl -s http://localhost:8081/realms/agentgateway/protocol/openid-connect/token \ -u agent:agent-secret -d grant_type=password \ -d username=testuser -d password=testpass | jq -r .access_token)Send the same request again. The gateway exchanges the Keycloak token at the Google STS, and forwards the request to Cloud Storage with the Google access token.
curl -s http://$INGRESS_GW_ADDRESS/storage/v1/b/$BUCKET/o \ -H "host: storage.example.com" \ -H "authorization: Bearer $INBOUND_TOKEN"Cloud Storage authorizes the request as the federated
testuseridentity, and returns the objects in the bucket.{ "kind": "storage#objects", "items": [ { "kind": "storage#object", "name": "hello.txt", "bucket": "agentgateway-te-my-project", ... } ] }Send a request without a token. The gateway has no subject token to exchange, so it rejects the request with a
400response and does not call the STS.curl -s -w " %{http_code}\n" http://$INGRESS_GW_ADDRESS/storage/v1/b/$BUCKET/o \ -H "host: storage.example.com"Example output:
invalid request 400
The exchange does not validate the incoming token itself; Google does. To reject an invalid or expired token at the gateway before it calls the STS, add a JWT authentication policy, as described in Validate the incoming token at the edge. Set preserveToken: true on that policy, so that the token is still in the request when the exchange runs.
Troubleshooting
When the exchange fails, the gateway returns 400 with the body invalid request. The response from the STS is logged at debug level. To see it, raise the log level of the token exchange module on the proxy, send the request again, and check the proxy logs.
kubectl port-forward deployment/agentgateway-proxy -n agentgateway-system 15000 &
curl -X POST "http://localhost:15000/logging?level=info,agentgateway::http::auth::oauth=debug"
kubectl logs deployment/agentgateway-proxy -n agentgateway-system | grep "oauth token exchange"The change lasts until the proxy restarts. Look for a message such as oauth token exchange rejected by authorization server, which includes the error from the STS.
| Error from the STS | Cause |
|---|---|
invalid_request: Invalid value for “audience”. | The audiences value is not the full resource name of a provider. It must start with //iam.googleapis.com/projects/, not https://. |
invalid_target: The target service indicated by the “audience” parameters is invalid. | The audiences value names a provider that does not exist or is disabled. Check the project number, pool ID, and provider ID. |
invalid_request: Invalid value of requested_token_type in request. | Set requestedTokenType to AccessToken. |
invalid_request: Scope(s) must be provided. | Set at least one value in scopes. |
invalid_grant | Google rejected the incoming token. Check that the token’s iss matches the provider’s issuer URI, that its aud is in the provider’s allowed audiences, that the token is not expired, and that the uploaded keys are current. |
If the exchange succeeds but Cloud Storage returns 403, the federated identity has no access to the bucket. Check that the IAM binding uses the project number and the sub claim of the token, and allow a few minutes for IAM changes to take effect.
Cleanup
You can remove the resources that you created in this guide.Delete the Kubernetes resources.
kubectl delete AgentgatewayPolicy google-token-exchange -n agentgateway-system kubectl delete httproute google-storage -n agentgateway-system kubectl delete AgentgatewayBackend google-storage -n agentgateway-system kubectl delete deployment keycloak -n agentgateway-system kubectl delete service keycloak -n agentgateway-system kubectl delete configmap keycloak-realm -n agentgateway-systemStop the port-forwards. Press
Ctrl+Cin the terminal that runs the Keycloak port-forward from Step 1. If you ran the admin port-forward in the troubleshooting section, stop it too.pkill -f "port-forward deployment/agentgateway-proxy" || trueDelete the Google Cloud resources.
gcloud storage rm --recursive gs://$BUCKET gcloud iam workload-identity-pools delete $POOL_ID --project $PROJECT_ID --location global --quietA deleted pool stays in a soft-deleted state for 30 days, and its ID cannot be reused during that time. To reuse the ID, restore the pool with
gcloud iam workload-identity-pools undelete.