GCP Service Account Impersonation and Escalation

GCP Service Account Impersonation and Escalation - article cover image Cloud Security
Time it takes to read this article 7 minutes.

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 containing iam.serviceAccounts.getAccessToken, iam.serviceAccounts.signJwt, iam.serviceAccounts.signBlob, or iam.serviceAccounts.actAs on 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-level roles/iam.serviceAccountKeyAdmin.
  • Optionally, iam.serviceAccountKeys.create permission, 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.
Bash

The 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.
JSON

A 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.
Bash

Walkthrough / 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.com
Bash

Confirm 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.com
Bash

If 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-admin
Bash

From 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.com generate GenerateAccessToken entries 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/owner on service accounts and human principals; use least-privilege predefined or custom roles instead.
  • Scope roles/iam.serviceAccountTokenCreator and roles/iam.serviceAccountUser narrowly — a single automation identity on a single target service account, never a group or project/folder level.
  • Enforce the iam.disableServiceAccountKeyCreation org policy to force short-lived impersonation tokens or Workload Identity Federation instead of downloadable keys.
  • Alert on google.iam.credentials.v1.IAMCredentials.GenerateAccessToken in Cloud Audit Logs, especially where caller and impersonated account diverge from an established baseline.
  • Alert on google.iam.admin.v1.CreateServiceAccountKey in 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

Copied title and URL