Skip to content

GCP_RUNS_AS

Copy page

Summary

Forestall ACL Alias GCP_RUNS_AS
GCP Alias Compute Execution & Pipeline Pivots
Affected Object Types Service Accounts
Exploitation Certainty Certain

Description

GCP_RUNS_AS represents that a GCE virtual machine runs as an attached service account. It points from the VM to the service account bound to it at creation time:

Virtual Machine ── GCP_RUNS_AS ──► Service Account

Attaching a service account to a VM makes that identity available to every process on the machine. The guest does not hold a key file; instead the metadata server at metadata.google.internal issues short-lived OAuth access tokens on request, with no authentication beyond being on the instance:

http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token

This is by design and is how workloads authenticate to Google APIs without managing credentials. Its consequence for the graph is that code execution on a VM is equivalent to holding the attached service account’s IAM privileges. GCP_RUNS_AS is the edge that makes that equivalence explicit, and it is what turns a shell into a privilege escalation.

Why the edge matters for attack paths

GCP_RUNS_AS is the pivot that converts machine access into identity. It completes several compound paths that are incomplete without it:

Preceding edge Combined effect
GCP_SSH_TO OS Login session yields the attached SA’s token
GCP_INJECT_SSH_KEY Planted key yields a shell, then the attached SA’s token
GCP_CREATE_COMPUTE + GCP_ACT_AS_SA Attacker creates a new VM with a chosen SA attached, establishing this edge deliberately to harvest that SA

The last is the important one. GCP_ACT_AS_SA on its own only grants the right to attach a service account to infrastructure — it is not directly exploitable. It becomes an escalation only when combined with the ability to create or modify a compute resource, because the new VM’s GCP_RUNS_AS edge is what actually surrenders the token. An attacker with both can attach any service account they can act as to a VM they control, and read its token out of the metadata server.

The default Compute Engine service account deserves particular attention. Unless explicitly overridden, GCP attaches it to new VMs and grants it roles/editor on the project if iam.automaticIamGrantsForDefaultServiceAccounts is not enforced. In that case, any VM running as it therefore has an edge to a project-editor identity, which itself carries GCP_CREATE_SA_KEYS and GCP_ACT_AS_SA. A single low-value VM can be the entry point to full project control through this chain.

Access scopes are a legacy secondary restriction on what a VM’s token may call. They are not a security boundary to rely on: https://www.googleapis.com/auth/cloud-platform is the common default and permits every API the service account’s IAM roles allow.

Identification

gcloud CLI

PROJECT_ID="my-project"
VM_NAME="example-vm"
VM_ZONE="example-zone"
# Map every VM to the service account it runs as, with scopes
gcloud compute instances list --project=$PROJECT_ID \
--format="table(name, zone, status, serviceAccounts[0].email, serviceAccounts[0].scopes.list())"
# Detail for a single instance
gcloud compute instances describe VM_NAME --zone=ZONE --project=$PROJECT_ID \
--format="value(serviceAccounts)"
# Find VMs running as the default Compute Engine SA (usually project editor)
PROJECT_NUM=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")
gcloud compute instances list --project=$PROJECT_ID \
--filter="serviceAccounts.email=${PROJECT_NUM}[email protected]" \
--format="table(name, zone, status)"
# Resolve what an attached SA can actually do — the real blast radius of the VM
gcloud projects get-iam-policy $PROJECT_ID --format=json | \
jq --arg sa "serviceAccount:$SA_EMAIL" \
'.bindings[] | select(.members[]? == $sa) | .role'
# Across the whole org, find every binding held by that SA
gcloud asset search-all-iam-policies \
--scope=organizations/$(gcloud organizations list --format="value(name)" | head -1 | cut -d/ -f2) \
--query="policy:$SA_EMAIL" \
--format="table(resource, policy.bindings.role)"

A VM is only as sensitive as the roles held by the service account it runs as and service accounts parented in one project frequently holds roles elsewhere.

Exploitation

GCP_RUNS_AS has no exploit of its own — the edge simply records an attachment, not a permission grant — but reaching it requires code execution on the VM, and cashing it in is immediate and unconditional. Its significance is what that execution is worth.

Given any shell on the instance, the attached identity is available immediately and without a credential:

# Which identity does this VM run as?
curl -s -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"
# What may its token be used for?
curl -s -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/scopes"
# Mint an access token
curl -s -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"

The token is a bearer credential usable from anywhere until it expires, so it can be exfiltrated and replayed off the instance:

TOKEN="ya29...."
# Enumerate reachable projects
curl -s -H "Authorization: Bearer $TOKEN" \
"https://cloudresourcemanager.googleapis.com/v1/projects"
# Enumerate secrets the SA can read
curl -s -H "Authorization: Bearer $TOKEN" \
"https://secretmanager.googleapis.com/v1/projects/my-project/secrets"

Where the SA is a project editor — as the default Compute Engine service account is — the path continues into durable persistence, since editor carries the ability to create service account keys:

gcloud auth activate-service-account --key-file=<(echo "$STOLEN_KEY_JSON")

Two properties make this pivot valuable to an attacker beyond the privileges themselves. Requests made with the token are attributed to the service account, not to the attacker’s own principal, so activity blends into normal workload traffic. And the credential is issued on demand by the metadata server, so there is no key file on disk to find and no rotation event to observe.

Mitigation

  1. Never leave the default Compute Engine service account attached. Sometimes it holds roles/editor on the project by default. Create a dedicated, minimally privileged service account per workload and attach that instead.
  2. Grant the attached SA only the roles its workload needs, at the narrowest scope that works. The VM’s blast radius is exactly this SA’s permission set — treat every role granted to it as granted to anyone who gets code execution on the machine.
  3. Do not attach an SA at all to VMs that make no Google API calls. An instance with no attached identity has no token to steal.
  4. Block metadata server access from workloads that do not need it, using network policy or host-level egress controls. This is the only control that severs the edge rather than limiting its value.
  5. Enforce the iam.automaticIamGrantsForDefaultServiceAccounts organization policy constraint so newly created default service accounts do not automatically receive roles/editor.
  6. Restrict roles/iam.serviceAccountUser (GCP_ACT_AS_SA) so attackers cannot choose which identity a new VM runs as. Pay particular attention to principals that hold it together with compute-create permissions — that combination lets them create this edge at will.
  7. Audit attachments of privileged service accounts to compute resources on a schedule, and alert when an SA with folder- or org-scoped roles is attached to a VM.

Detection

Log Type Method Key Fields
Admin Activity v1.compute.instances.insert serviceAccounts[].email on the new instance — the edge being created
Admin Activity v1.compute.instances.setServiceAccount Attached identity changed on an existing VM
Admin Activity SetIamPolicy New roles granted to an SA that is attached to VMs
Data Access Token use by SA authenticationInfo.principalEmail is the SA; compare callerIp against the VM’s egress
PROJECT_ID="my-project"
# VMs created or reconfigured with an attached service account
gcloud logging read \
'log_id("cloudaudit.googleapis.com/activity")
AND protoPayload.serviceName="compute.googleapis.com"
AND protoPayload.methodName=~"v1\.compute\.instances\.(insert|setServiceAccount)"' \
--project="$PROJECT_ID" \
--freshness=400d \
--limit=1000 \
--order=desc \
--format=json | jq '.[] | {
time: .timestamp,
actor: .protoPayload.authenticationInfo.principalEmail,
method: .protoPayload.methodName,
resource: .protoPayload.resourceName,
attachedSA: (.protoPayload.request.serviceAccounts // [])
}'
# Activity performed by an attached SA, to baseline its normal caller IPs
gcloud logging read \
"protoPayload.authenticationInfo.principalEmail=\"$SA_EMAIL\"" \
--project=$PROJECT_ID \
--format="table(timestamp, protoPayload.methodName, protoPayload.requestMetadata.callerIp)"

Alert on:

  • VMs created with a service account that holds folder- or organization-scoped roles.
  • setServiceAccount on an existing VM — reattaching a different identity is uncommon in steady-state operations and is a direct attempt to acquire this edge.
  • Any VM created with the default Compute Engine service account attached, given its default roles/editor grant.
  • Activity attributed to a VM-attached service account originating from an IP outside that VM’s egress range, which indicates the token was exfiltrated rather than used in place.
  • An attached service account calling APIs unrelated to its workload’s normal behaviour, particularly iam.serviceAccounts.keys.create or Secret Manager reads it has never made before.
  • The same principal holding roles/iam.serviceAccountUser and compute-create permissions, which allows this edge to be established on demand.

References

Was this page helpful?

Thank you for your feedback.