Workload Identity for GKE Explained: Secure Keyless Access to Google Cloud APIs
Running workloads in GKE that need to call Google Cloud APIs — Cloud Storage, Pub/Sub, Secret Manager, BigQuery — requires credentials. The naive approach is to download a service account key file and mount it as a Kubernetes Secret. Workload Identity solves this properly: pods receive short-lived, automatically refreshed Google OAuth tokens with no key file ever created, stored, or distributed. This page explains what Workload Identity is, why it matters, and how to configure it from scratch.
The simple explanation
Think of Workload Identity as a badge system for pods.
Without it, every pod that needs to call a Google Cloud API must carry a credential — usually a JSON key file that acts like a permanent password. If that file is copied, leaked, or committed to a repo, it works forever and from anywhere on the internet.
With Workload Identity, pods carry no credential at all. Instead, when a pod needs a token, GKE vouches for it. It says: “This pod is running as Kubernetes ServiceAccount my-app-ksa in the production namespace, and I trust it to act as Google service account my-app-gsa@my-project-id.iam.gserviceaccount.com.” Google’s infrastructure checks that trust relationship and issues a short-lived token, valid for one hour, then discards it.
Distributing a service account key file is like giving every new employee a master key to the building on their first day. If anyone loses their key, forgets it in a bag, or accidentally posts a photo of it, the entire building is at risk. And you have no easy way to know how many copies exist. Workload Identity is more like a keycard system: each employee has an identity in the system, the door checks who they are in real time, and access can be updated or revoked instantly with no physical key to track down.
The three pieces involved:
- Kubernetes ServiceAccount (KSA): a namespaced Kubernetes resource that provides an identity to pods. Every pod runs as a KSA. If you do not specify one, it uses the
defaultKSA in its namespace. - Google service account (GSA): a GCP IAM identity with its own set of permissions. This is the account whose credentials the pod will receive.
- Short-lived token: a Google OAuth access token, valid for one hour, issued automatically by the GKE metadata server via Google’s Security Token Service. The pod never sees or stores a private key.
The pod does not carry a key. It has an identity. GKE and GCP together convert that identity into a usable credential, automatically, every time the pod needs one.
What is Workload Identity for GKE?
Workload Identity for GKE is a GCP feature that creates a trusted, explicit mapping between a Kubernetes ServiceAccount and a Google Cloud IAM service account. Once configured, any pod that runs under the annotated KSA automatically obtains credentials for the linked GSA: no key file, no manual credential rotation, and no changes to application code.
It sits at the intersection of Kubernetes identity and Google Cloud IAM. The KSA is a Kubernetes-native concept; the GSA belongs to GCP’s IAM system. Workload Identity creates a bridge between the two, allowing the cluster to act on behalf of a specific GCP identity in a controlled, auditable way.
Applications running in GKE pods typically access Google Cloud using the official client libraries. These libraries use Application Default Credentials (ADC), which automatically discover credentials from the environment. When running inside a GKE pod with Workload Identity configured, ADC queries the GKE metadata server and receives the token for the linked GSA with zero changes to the application itself.
This is the recommended approach for any GKE workload that needs to call Google Cloud APIs. The securing GKE clusters page covers how Workload Identity fits into the broader GKE security model.
Why Workload Identity matters
The core problem it solves is the service account key file. Understanding why key files are dangerous makes it clear why Workload Identity is the right replacement.
A service account key file is a JSON document containing a private RSA key. It grants its holder the full permissions of that service account, indefinitely, until manually revoked. You cannot detect if someone copied it. You cannot limit where it can be used from. You can only revoke the entire key, which breaks every system using it at once.
Key files spread silently#
Once you create a key file, it tends to multiply. It lands in a Kubernetes Secret. It gets copied into a CI/CD variable store. It gets embedded in a Docker image during a rushed build. It ends up committed to a repo. Each copy is a separate exposure surface, and most teams cannot reliably track how many copies exist.
Kubernetes Secrets are not encrypted by default#
Storing a key file in a Kubernetes Secret does not encrypt it. It base64-encodes it. Anyone with access to etcd, or with sufficient RBAC permissions, can retrieve the raw value. See managing secrets in Kubernetes for the full picture.
Blast radius is unbounded#
A single leaked key grants all the permissions of that service account to anyone in possession of it, from any network, at any time. There is no automatic expiry and no inherent scope limit.
What Workload Identity gives you instead#
- No persistent credential to leak. Tokens live in memory only and expire after one hour.
- Automatic rotation. The GKE metadata server refreshes tokens before they expire with no manual process required.
- Least-privilege enforcement. Each workload gets its own narrowly scoped GSA. See principle of least privilege.
- Clean audit trail. Cloud Audit Logs show API calls attributed to the specific GSA, making it clear which workload made which call.
- No operational overhead. No key rotation schedules, no key revocation incidents, no broken deployments caused by expired credentials.
For a deeper look at why key files are risky, read why service account keys are dangerous.
Workload Identity vs service account keys
This is one of the most common decisions teams face when building on GKE. The table below captures the key differences:
| Workload Identity | Service account key file | |
|---|---|---|
| Credential type | Short-lived OAuth token (1 hour) | Long-lived RSA private key (no expiry) |
| Rotation | Automatic, transparent to the application | Manual, must be rotated and redeployed |
| Storage risk | Nothing stored on disk or in a Secret | Key file must be stored somewhere accessible to the pod |
| Blast radius if leaked | Minimal, token expires within an hour | Severe, key works indefinitely from anywhere |
| Scope | Bound to the specific KSA in a specific namespace | Works from any machine, any IP |
| Operational overhead | Configure once, no ongoing credential management | Ongoing: rotation schedules, revocation, tracking copies |
| Application code changes | None, ADC detects automatically | Usually none, but key file path must be passed or mounted |
| Recommended for GKE | Yes, default for all new workloads | No, treat as a last resort for legacy systems only |
Workload Identity should be the default for every GKE workload that needs Google Cloud API access. Service account key files should be used only in situations where Workload Identity genuinely cannot work, and even then with tight constraints and a plan to migrate.
If you have existing workloads using key files, replacing them with Workload Identity is one of the highest-value security improvements you can make. The service account keys explained page covers the full lifecycle and risks.
How Workload Identity works
Here is the sequence of events when a pod calls a Google Cloud API using Workload Identity:
- Pod starts under a Kubernetes ServiceAccount (KSA) that has been annotated with a GSA email.
- Pod calls the metadata server at
169.254.169.254(ormetadata.google.internal). This happens automatically when the application uses a Google Cloud client library via ADC. No explicit code is needed. - GKE metadata server intercepts the request. Each node runs a metadata server DaemonSet (enabled with
--workload-metadata=GKE_METADATAon the node pool). It identifies the pod’s KSA from the Kubernetes API. - The metadata server calls Google’s Security Token Service (STS). It presents the pod’s KSA identity token and requests a Google OAuth access token for the linked GSA.
- STS validates the binding. It checks that the KSA has the annotation pointing to the GSA, and that the GSA has an IAM binding granting
roles/iam.workloadIdentityUserto the KSA’s workload identity principal. Both must be in place. - A short-lived token is returned. Valid for one hour, scoped to the GSA’s permissions. The token is returned to the application via the metadata server response.
- Application uses the token. The client library uses it to authenticate API calls. The token is refreshed automatically before it expires.
The pod says “I need credentials.” GKE says “This pod is allowed to act as this GSA.” GCP issues a temporary token. The pod makes its API calls. The token expires. Nothing persists.
The two required conditions for this to work:
- The KSA must be annotated with
iam.gke.io/gcp-service-account: GSA_EMAIL - The GSA must have an IAM binding granting
roles/iam.workloadIdentityUsertoserviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]
Either condition alone is not enough. Both must be in place before token exchange works.
When to use Workload Identity
Use Workload Identity for any GKE workload that needs to call a Google Cloud API. Some realistic examples:
- A web service reading user files from Cloud Storage. The pod needs
roles/storage.objectVieweron the relevant bucket. - A background worker publishing events to Pub/Sub. The pod needs
roles/pubsub.publisheron the specific topic. - An application reading secrets at startup from Secret Manager. The pod needs
roles/secretmanager.secretAccessoron specific secret versions. See managing secrets in Kubernetes. - A batch job querying BigQuery. The pod needs
roles/bigquery.dataViewerandroles/bigquery.jobUser. - A service writing structured logs to Cloud Logging. The pod needs
roles/logging.logWriter.
In all of these cases, the pod needs a Google Cloud identity. Workload Identity is the right way to provide one.
If a pod does not call any Google Cloud API, it does not need Workload Identity. Do not annotate KSAs unless the workload actually requires GCP access. Unnecessary bindings add surface area without adding value.
For workloads running outside GKE that need Google Cloud access, such as CI/CD pipelines or external systems, see Workload Identity Federation, which extends the same keyless principle to non-GCP environments.
Step-by-step setup
The following steps configure Workload Identity for a single workload. All values use my-project-id, my-cluster, my-app-gsa, my-app-ksa, and the production namespace as consistent examples throughout.
Step 1: Enable Workload Identity on the cluster#
Pass --workload-pool when creating a new cluster. The workload pool identifier is always PROJECT_ID.svc.id.goog:
gcloud container clusters create my-cluster \
--region=europe-west2 \
--workload-pool=my-project-id.svc.id.googTo enable Workload Identity on an existing cluster:
gcloud container clusters update my-cluster \
--region=europe-west2 \
--workload-pool=my-project-id.svc.id.googGKE Autopilot clusters have Workload Identity enabled by default. You do not need to pass —workload-pool. Skip straight to Step 3.
Step 2: Update the node pool metadata mode#
The GKE metadata server must run on every node. On Standard clusters, this requires setting the metadata mode to GKE_METADATA on each node pool:
gcloud container node-pools update default-pool \
--cluster=my-cluster \
--region=europe-west2 \
--workload-metadata=GKE_METADATAIf the node pool is not updated to GKE_METADATA, pods will contact the Compute Engine instance metadata server directly and receive node-level credentials instead of Workload Identity credentials. This bypasses the entire setup silently — the pod still runs, but it is not using the identity you think it is. Always verify this is set on every node pool. See node pools for more on node pool configuration.
Step 3: Create the Google service account (GSA)#
This is the GCP identity whose permissions the pod will use:
gcloud iam service-accounts create my-app-gsa \
--display-name="My Application GSA" \
--project=my-project-idStep 4: Grant the GSA the required IAM roles#
Grant the narrowest set of permissions the workload actually needs. For example, read access to a specific Cloud Storage bucket:
gcloud storage buckets add-iam-policy-binding gs://my-data-bucket \
--member="serviceAccount:my-app-gsa@my-project-id.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"Do not grant broad roles like roles/editor or roles/storage.admin unless the workload genuinely requires that scope. See principle of least privilege.
Step 5: Create the Kubernetes ServiceAccount (KSA)#
Create the KSA in the correct namespace. The namespace matters because it is part of the workload identity principal used in the IAM binding:
kubectl create serviceaccount my-app-ksa \
--namespace=productionOr declare it in YAML for version control:
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-ksa
namespace: productionStep 6: Annotate the KSA with the GSA email#
The annotation tells GKE which GSA this KSA should map to:
kubectl annotate serviceaccount my-app-ksa \
--namespace=production \
iam.gke.io/gcp-service-account=my-app-gsa@my-project-id.iam.gserviceaccount.comOr declare this in the YAML manifest:
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-ksa
namespace: production
annotations:
iam.gke.io/gcp-service-account: my-app-gsa@my-project-id.iam.gserviceaccount.comThe annotation alone is not sufficient. The IAM binding in the next step is also required.
Step 7: Grant roles/iam.workloadIdentityUser on the GSA#
This IAM binding tells GCP to trust the Kubernetes ServiceAccount as an authorised impersonator of the GSA:
gcloud iam service-accounts add-iam-policy-binding \
my-app-gsa@my-project-id.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:my-project-id.svc.id.goog[production/my-app-ksa]"The --member format is always:
serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]Every part of this string matters. Getting the project ID, namespace, or KSA name wrong means the binding does not match and token exchange fails. See service account impersonation for how impersonation works more broadly in GCP.
Step 8: Reference the KSA in your Deployment#
Specify serviceAccountName in the pod spec. Without this, the pod runs under the default KSA and Workload Identity does not apply:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
spec:
replicas: 2
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
serviceAccountName: my-app-ksa
containers:
- name: app
image: europe-west2-docker.pkg.dev/my-project-id/my-repo/my-app:latestSee Deployments in Kubernetes for a full reference on Deployment configuration.
Step 9: Verify from inside a pod#
Run a temporary pod under the KSA and confirm the identity is working:
kubectl run -it --rm workload-identity-test \
--image=google/cloud-sdk:slim \
--namespace=production \
--overrides='{"spec": {"serviceAccountName": "my-app-ksa"}}' \
-- bashInside the pod:
# Should show the GSA email as the active identity
gcloud auth list
# Call the metadata server directly to confirm the email
curl -s -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"If configured correctly, both commands return my-app-gsa@my-project-id.iam.gserviceaccount.com.
Common mistakes
1. Annotating the GSA instead of the KSA.
The iam.gke.io/gcp-service-account annotation belongs on the Kubernetes ServiceAccount object, not on the Google Cloud IAM service account. This is easy to confuse because both entities are called “service accounts.” The annotation lives in the KSA’s metadata.annotations in Kubernetes.
2. Forgetting the IAM binding.
Annotating the KSA is only half of the setup. GCP also needs an IAM binding granting roles/iam.workloadIdentityUser to the KSA’s workload identity principal on the GSA. Without this binding, the Security Token Service rejects the exchange and the pod receives an authentication error. The annotation and the IAM binding must both be present.
3. Wrong namespace or KSA name in the member string.
The --member value serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME] is case-sensitive and must match exactly. Using the wrong namespace, a typo in the KSA name, or the wrong project ID produces a binding that appears to succeed but never matches the pod’s identity.
4. Not setting serviceAccountName in the pod spec.
If serviceAccountName is omitted from the pod spec, the pod runs under the default KSA in its namespace. Unless that KSA is also annotated and bound (which it should not be), Workload Identity does not apply to that pod. Always be explicit.
5. Updating the cluster but not the node pool.
Enabling Workload Identity on a cluster with --workload-pool does not automatically update existing node pools. Each node pool must also be updated to --workload-metadata=GKE_METADATA. If this step is missed, the metadata server DaemonSet does not run on those nodes, and pods silently fall back to using the node’s Compute Engine service account credentials.
6. Assuming a Kubernetes Secret is a secure key store. Kubernetes Secrets are base64-encoded, not encrypted. Storing a service account key in a Secret is not a secure solution. Workload Identity removes the need for this pattern entirely because there is no key file to protect.
7. Expecting Workload Identity to compensate for over-permissive IAM roles.
Workload Identity controls how credentials are issued, not what those credentials can do. If the GSA has roles/editor on the project, the pod has roles/editor. Always grant the GSA the narrowest set of permissions the workload actually needs.
Troubleshooting
| Symptom | Likely cause |
|---|---|
Pod gets 403 Permission Denied on a Google Cloud API call | IAM binding on the GSA is missing or incorrect, or the GSA does not have the required role for that API |
gcloud auth list inside the pod shows the compute service account, not the GSA | Node pool --workload-metadata is not set to GKE_METADATA, so the pod is bypassing the Workload Identity metadata server |
| Metadata server call returns an error or empty response | Pod is not running under the annotated KSA; check serviceAccountName in the pod spec |
Token exchange fails with UNAUTHENTICATED | IAM binding roles/iam.workloadIdentityUser is missing, or the --member string does not exactly match the KSA’s namespace and name |
| Workload Identity appears configured but API calls use node credentials | Cluster has Workload Identity enabled but the specific node pool does not have GKE_METADATA mode active; check with gcloud container node-pools describe |
| API calls fail immediately after setup | GCP IAM policy propagation can take up to 60 seconds. Wait briefly and retry before assuming a configuration error |
annotate command succeeds but Workload Identity still does not work | Double-check the annotation key is exactly iam.gke.io/gcp-service-account and the value is the full GSA email in the format NAME@PROJECT_ID.iam.gserviceaccount.com |
Run: gcloud container node-pools describe default-pool —cluster=my-cluster —region=europe-west2 —format=“value(config.workloadMetadataConfig.mode)”. The output should be GKE_METADATA. If it is not, run the update command in Step 2.
Workload Identity and OAuth scopes
Node pools created before Workload Identity was common often have restrictive OAuth scopes, for example storage-ro or compute-rw. When using Workload Identity, the node’s OAuth scopes no longer limit what the GSA can do.
Here is why: when a pod uses Workload Identity, the token it receives is issued by Google’s Security Token Service for the GSA specifically. It is not a node token with scoped-down access. It is a service account token. The effective permissions of that token are determined entirely by the IAM roles granted to the GSA, not by the node pool’s OAuth scopes.
This means you can grant a GSA very narrow permissions — say, only roles/pubsub.publisher on a specific topic — and the pod has exactly that capability and nothing else, regardless of what OAuth scopes the node pool was created with. IAM on the GSA is the single authoritative source of truth for what the pod can do.
Best practices
- One KSA per workload or trust boundary. Do not share a KSA across unrelated deployments. If two services have different access requirements, they need different KSAs and different GSAs.
- One narrowly scoped GSA per workload. Create a dedicated GSA for each application with only the IAM roles it genuinely needs. Avoid reusing GSAs across workloads.
- Grant minimal IAM roles. Prefer resource-level bindings (on a specific bucket or topic) over project-level bindings where possible. Review and remove roles that are no longer needed.
- Version-control your KSA manifests. Declare Kubernetes ServiceAccounts in YAML with the annotation included and commit them to your repository. This makes the binding explicit and reviewable.
- Do not annotate the default KSA. Annotating the
defaultKSA in a namespace effectively grants Workload Identity credentials to any pod that does not specify aserviceAccountName, which is the opposite of least privilege. - Verify Workload Identity in each environment. Run the metadata server verification step in staging before promoting to production. A misconfigured binding fails silently until a pod tries to call an API.
- Do not mix key-based auth and Workload Identity. Once a workload uses Workload Identity, remove any key files from Kubernetes Secrets in that namespace. Mixing the two patterns creates confusion about which credential is actually being used.
For managing service accounts more broadly, including creation, lifecycle, and IAM assignments, see the fundamentals page.
Summary
- Workload Identity for GKE lets pods access Google Cloud APIs without service account key files. It replaces long-lived static credentials with short-lived, automatically refreshed tokens.
- It works by binding a Kubernetes ServiceAccount (KSA) to a Google Cloud IAM service account (GSA). When a pod runs under the KSA, GKE’s metadata server exchanges its identity for a one-hour token via Google’s Security Token Service.
- Two conditions must both be present: the KSA must be annotated with
iam.gke.io/gcp-service-account: GSA_EMAIL, and the GSA must have an IAM binding grantingroles/iam.workloadIdentityUsertoserviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]. - On Standard clusters, enable Workload Identity at the cluster level with
—workload-pool=PROJECT_ID.svc.id.googand at the node pool level with—workload-metadata=GKE_METADATA. On Autopilot, it is enabled by default. - Google Cloud client libraries pick up Workload Identity credentials automatically via Application Default Credentials. No code changes are required.
- Service account key files should not be used for GKE workloads. Workload Identity is more secure, requires no credential rotation, and reduces operational overhead significantly.
- Verify the binding from inside a running pod using
gcloud auth listor a direct metadata server call before deploying to production.
Frequently asked questions
What is Workload Identity in GKE?
Workload Identity for GKE is a mechanism that links a Kubernetes ServiceAccount (KSA) to a Google Cloud IAM service account (GSA). When a pod runs under that KSA, it automatically obtains short-lived Google OAuth tokens via the GKE metadata server without needing a service account key file stored anywhere.
Is Workload Identity enabled by default in GKE Autopilot?
Yes. Workload Identity is enabled by default on all GKE Autopilot clusters and cannot be disabled. You still need to create the Kubernetes ServiceAccount, annotate it with the GSA email, and grant the roles/iam.workloadIdentityUser IAM binding, the same as on Standard clusters. The only difference is that you do not pass --workload-pool when creating the cluster.
Do I need to change application code to use Workload Identity?
No. Google Cloud client libraries use Application Default Credentials (ADC), which query the GKE metadata server automatically when running inside a GKE pod. If Workload Identity is configured correctly, ADC receives a short-lived OAuth token scoped to the linked Google service account. No code changes are required.
What is the difference between a Kubernetes ServiceAccount and a Google service account?
A Kubernetes ServiceAccount (KSA) is a namespaced Kubernetes resource that provides an identity to pods within a cluster. A Google service account (GSA) is a GCP IAM identity that can hold permissions to call Google Cloud APIs. Workload Identity creates a trusted bridge between the two: the KSA identity maps to the GSA, so pods can act as the GSA without holding a key file.
Why is Workload Identity safer than service account keys?
Service account key files are long-lived static credentials that never expire automatically. If one leaks in a Git commit, a container image, or a misconfigured bucket, an attacker can use it indefinitely from anywhere in the world. Workload Identity issues tokens that expire after one hour, are never written to disk, and are only accessible to pods running the correct Kubernetes ServiceAccount in the correct namespace. There is no file to leak.