# Connect GKE workloads with workload identity

Every pod on a Google Kubernetes Engine (GKE) cluster runs as a Kubernetes service account, and the cluster can issue the pod a short-lived token that proves it. With [workload identity](/docs/ai-control-plane/identity/agents/workload-identity), the pod exchanges that token for a Speakeasy access token and calls MCP servers as a Speakeasy agent. Nothing secret is stored in the cluster, and nothing needs rotating by hand.

This guide covers:

- [How the connection works](#how-the-connection-works)
- [Prerequisites](#prerequisites)
- [Start the setup](#start-the-setup)
- [Enter the cluster](#enter-the-cluster)
- [Choose the workload](#choose-the-workload)
- [Give the pod a Speakeasy token](#give-the-pod-a-speakeasy-token)
- [Exchange the token and call a server](#exchange-the-token-and-call-a-server)
- [Test from the pod](#test-from-the-pod)
- [Manage and revoke access](#manage-and-revoke-access)

## How the connection works

When a pod needs to call an MCP server:

- The kubelet writes a token for the pod's service account to a file in the pod, issued for Speakeasy's audience and signed by the cluster. Its subject has the form `system:serviceaccount:NAMESPACE:SERVICE_ACCOUNT`.
- The pod posts the token to the Speakeasy token endpoint.
- Speakeasy checks the signature against the cluster's published keys and matches the subject against the access rule saved for the agent.
- Speakeasy returns an access token valid for 15 minutes. The pod sends it on MCP requests and exchanges a new token when it expires.

Each GKE cluster signs its own tokens, so each connected cluster is trusted separately, and a rule on one cluster admits nothing on another. The rule names one namespace and service account, so only pods running as that service account act as the agent, and they can do only what the agent's policy allows.

## Prerequisites

- A GKE cluster, and `kubectl` access to it.
- Permission to edit the workload's manifests.
- In Speakeasy: `workload:write`, which the Admin role holds by default.
- MCP servers attached to a Speakeasy authorization server in shared mode for the organization or project. If the setup reports that no token endpoint is available, contact Speakeasy to enable one.

GKE publishes every cluster's signing keys on `container.googleapis.com`, including clusters with a private control plane, so Speakeasy can verify their tokens without network access to the cluster.

## Start the setup

Open **Identity > Agents** in the project sidebar and select **New agent identity**. Name the agent, select **Continue**, then choose the MCP servers the workload may call. The agent reaches nothing else, and never more than the person creating it can reach.

On the credential step, choose **Workload identity**, then choose **GKE** under **Platform** and select **Continue**.

## Enter the cluster

To list the clusters in a project with their locations, run:

```bash
gcloud container clusters list --format="table(name,location)"
```

Enter the cluster's **Project ID**, **Location**, and **Cluster name**. The location is the cluster's region, such as `us-central1`, or its zone, such as `us-central1-a`.

Once all three are valid, the step shows the issuer Speakeasy will trust. To confirm it, point `kubectl` at the cluster and run:

```bash
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
```

The output must match the issuer the step shows exactly, in this form:

```text
https://container.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/clusters/CLUSTER_NAME
```

If it doesn't match, check the project, location, and cluster name. Select **Continue**.

## Choose the workload

Enter the **Namespace** the workload's pods run in and the **Service account** they run as, which is the `serviceAccountName` in the pod spec. Pods that don't set one run as `default`.

Select **Connect GKE**. Speakeasy creates the agent, trusts the cluster, and saves one access rule:

```text
system:serviceaccount:NAMESPACE:SERVICE_ACCOUNT
```

The rule is exact. A pod running as any other service account, or in any other namespace, is refused. To let a second workload act as the same agent, add a rule for its own service account.

> **Give each workload its own service account**
> Every pod running as the service account acts as the agent, including pods deployed later by anyone who can create pods in that namespace. Use a dedicated service account for the workload rather than `default`, and choose an agent whose policy reaches only what that workload needs.

## Give the pod a Speakeasy token

Add a projected service account token to the workload's pod spec, issued for Speakeasy's audience. The setup shows this manifest with the namespace, service account, and audience filled in. For a Deployment, the same fields go under `spec.template.spec`:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-workload
  namespace: NAMESPACE
spec:
  selector:
    matchLabels:
      app: my-workload
  template:
    metadata:
      labels:
        app: my-workload
    spec:
      serviceAccountName: SERVICE_ACCOUNT
      containers:
        - name: app
          image: my-image
          env:
            - name: SPEAKEASY_TOKEN_FILE
              value: /var/run/secrets/speakeasy/token
          volumeMounts:
            - name: speakeasy-token
              mountPath: /var/run/secrets/speakeasy
              readOnly: true
      volumes:
        - name: speakeasy-token
          projected:
            sources:
              - serviceAccountToken:
                  path: token
                  audience: SPEAKEASY_AUDIENCE
                  expirationSeconds: 600
```

`SPEAKEASY_AUDIENCE` is the issuer URL of the Speakeasy authorization server, shown in the setup. Speakeasy refuses a token issued for any other audience.

The token sits in its own directory, so it doesn't replace the service account token Kubernetes mounts by default, and it works beside GKE Workload Identity Federation for Google Cloud APIs. Both tokens come from the same service account, but each is issued for its own audience.

> **Keep the token lifetime at 600 seconds**
> Speakeasy accepts each token only once, and refuses a token valid for more than an hour. The kubelet writes a new token after about 80% of its lifetime, so at 600 seconds a fresh token is ready roughly every 8 minutes, well before the 15-minute access token runs out. A longer lifetime leaves the pod holding a token it has already used.

## Exchange the token and call a server

Before calling an MCP server, the workload reads the token file and posts it to the token endpoint shown in the setup, using the OAuth JWT bearer grant. No client ID or secret is involved:

```bash
TOKEN_ENDPOINT=https://AUTHENTICATION_HOST/oauth/usi/ISSUER_ID/token
MCP_URL=https://MCP_HOST/mcp/SERVER_SLUG

response=$(curl -sS "$TOKEN_ENDPOINT" \
  --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  --data-urlencode "assertion=$(cat "$SPEAKEASY_TOKEN_FILE")")
access_token=$(printf '%s' "$response" | jq -r '.access_token // empty')
if [ -z "$access_token" ]; then echo "Exchange failed: $response" >&2; exit 1; fi

curl -sS --fail-with-body "$MCP_URL" \
  -H "Authorization: Bearer $access_token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

If the exchange is refused, the script prints the token endpoint's response, such as `invalid_grant`, and stops before calling the server. `MCP_URL` is the URL on the MCP server's page in Speakeasy. The access token works at every MCP server of the authorization server that the agent may connect to.

In an application, keep the access token and reuse it until it expires, then read the file again and exchange the new token. Read the file each time rather than caching its contents, because the kubelet replaces the token in place. Exchange once per access token, from one place in the pod: two containers or processes exchanging the same token file at once means one of them is refused.

## Test from the pod

Run the exchange from a pod of the workload:

```bash
kubectl exec -n NAMESPACE deploy/my-workload -- sh -c 'test -s "$SPEAKEASY_TOKEN_FILE" && echo "token present"'
```

This confirms the kubelet wrote the token. Then run the exchange from the previous section in the pod. The `tools/list` request returns the tools the agent's policy allows.

Each pod that signs in appears on [MCP Sessions](/docs/ai-control-plane/mcp-gateway/access/clients-and-sessions#organization-wide-view) under the cluster's issuer and the service account's subject.

If the exchange fails with `invalid_grant`, check:

- The token's audience matches the Speakeasy issuer URL exactly, including the scheme and any trailing path.
- The pod runs in the namespace and as the service account in the access rule.
- The project, location, and cluster name match the issuer `kubectl` reports.
- The token hasn't been exchanged already. Wait for the kubelet to write a new one, or lower `expirationSeconds` to 600.
- The agent's policy allows the server, and the agent owner still has access to it.

Speakeasy returns the same `invalid_grant` error for every refusal, so these checks are the quickest way to find the cause.

## Manage and revoke access

The agent's **Credentials** section lists the platforms that sign in as it, under **Federated access**. Each connected cluster appears by name, such as **GKE my-cluster (my-project, us-central1)**.

- To change the servers the workload can reach, select **Edit** on the agent and save a new selection.
- To connect another cluster, run the setup again for that cluster. Each cluster is trusted separately.
- To disconnect a cluster, select **Remove** on its row, then remove the projected token from the workload's manifest.

Removing access stops new exchanges but doesn't end sessions already issued, which last up to 15 minutes. To cut off a workload immediately, also revoke its sessions on **MCP Sessions**, or [revoke the agent](/docs/ai-control-plane/identity/agents#suspend-revoke-or-delete-an-agent). Every change is recorded in [Audit Logs](/docs/ai-control-plane/org-admin/audit-logs).

## Next steps

- Read [Workload identity](/docs/ai-control-plane/identity/agents/workload-identity) for how trusted platforms, access rules, and token exchange work.
- Read [Authorizing agents](/docs/ai-control-plane/identity/agents/authorizing-agents) to scope the agent the workload acts as.

---

Index of every Speakeasy page for agents: [llms.txt](https://www.speakeasy.com/llms.txt). AI control plane and MCP gateway content in one file: [llms-full.txt](https://www.speakeasy.com/llms-full.txt). Any page is available as markdown by appending `.md` to its URL or by requesting it with `Accept: text/markdown`.
