Back to blog
Best PracticesCloud SecurityCompute & ContainersGCPIdentity & Access

VM Service Account Has Full API Access: Why It Matters and How to Fix It

Learn why a Compute Engine VM with full Cloud API access is a serious risk, how attackers exploit it via the metadata server, and how to fix and prevent it.

TL;DR

This check flags Compute Engine VMs whose attached service account is configured with the cloud-platform scope, granting full access to every Google Cloud API. If that VM is compromised, an attacker inherits all the permissions the service account holds. Fix it by setting narrowly scoped access scopes or, better, by relying on dedicated least-privilege service accounts with specific IAM roles.

Access scopes are one of the oldest and most misunderstood parts of Compute Engine. They predate fine-grained IAM, and many teams still leave VMs running with the legacy "Allow full access to all Cloud APIs" setting because it was the path of least resistance when the instance was created. This check catches exactly that situation, and it is worth understanding why a single dropdown choice can quietly widen your blast radius across an entire project.


What this check detects

The compute_fullapiaccess check inspects each Compute Engine instance and looks at the OAuth access scopes assigned to its attached service account. It flags any VM that uses the https://www.googleapis.com/auth/cloud-platform scope, which Google labels in the console as "Allow full access to all Cloud APIs."

When that scope is present, the VM can request tokens for any Google Cloud API on behalf of its service account. The only thing standing between a workload and your entire cloud surface is the IAM roles granted to that service account.

Note: Access scopes and IAM roles are two separate layers. Scopes define which APIs a token can talk to, and IAM defines what that identity is allowed to do. The effective permission a VM has is the intersection of the two. Full API scope removes the scope-level guardrail entirely, leaving IAM as the only control.


Why it matters

The default Compute Engine service account often carries the Editor role on the whole project. Combine that with the full-API-access scope and you have a VM that can read and write nearly everything: storage buckets, BigQuery datasets, Pub/Sub topics, secrets, and other compute instances.

Here is the attack path that makes this dangerous in practice.

  1. An attacker exploits a vulnerable web app or SSRF flaw running on the VM.
  2. They query the metadata server at http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token to grab an access token.
  3. Because the scope is cloud-platform, that token works against every API the service account's IAM roles permit.
  4. With Editor and full scope, they pivot to read secrets, exfiltrate storage, spin up crypto-mining instances, or move laterally.

SSRF and metadata server abuse is not theoretical. It is one of the most common cloud breach patterns, and the 2019 Capital One incident on AWS followed an almost identical shape. The single difference that determines whether an SSRF turns into a full project compromise is how much that VM's identity is allowed to do.

A VM should be able to do its job and nothing more. If your web server needs to read one bucket, it should not hold a token that can also delete your production database.


How to fix it

You have two real options, and the second is the one you actually want long term.

Option 1: Narrow the access scopes

Replace the full-access scope with specific scopes matching what the workload needs. Scopes cannot be changed on a running instance, so you must stop the VM first.

Warning: Updating access scopes requires stopping the instance. This means downtime for that VM. Schedule the change during a maintenance window or roll it out on a replacement instance behind a load balancer to avoid disruption.

# Stop the instance first
gcloud compute instances stop my-instance --zone=us-central1-a

# Apply specific scopes instead of cloud-platform
gcloud compute instances set-service-account my-instance \
  --zone=us-central1-a \
  [email protected] \
  --scopes=https://www.googleapis.com/auth/devstorage.read_only,https://www.googleapis.com/auth/logging.write,https://www.googleapis.com/auth/monitoring.write

# Start it back up
gcloud compute instances start my-instance --zone=us-central1-a

Option 2: Use cloud-platform scope with a least-privilege service account (recommended)

Google's own current guidance is to set the scope to cloud-platform and control access entirely through IAM roles, rather than trying to manage the legacy granular scopes. The catch is that this only works safely when the service account itself is tightly scoped. The problem this check flags is not the scope alone, it is full scope sitting on top of an over-permissioned default service account.

So the durable fix is to create a dedicated service account with only the roles the workload needs, then attach it to the VM.

# Create a dedicated service account for the workload
gcloud iam service-accounts create app-vm-sa \
  --display-name="App VM service account"

# Grant only the roles this workload actually needs
gcloud projects add-iam-policy-binding my-project \
  --member="serviceAccount:[email protected]" \
  --role="roles/storage.objectViewer"

gcloud projects add-iam-policy-binding my-project \
  --member="serviceAccount:[email protected]" \
  --role="roles/logging.logWriter"

# Attach the scoped service account to the stopped instance
gcloud compute instances stop my-instance --zone=us-central1-a

gcloud compute instances set-service-account my-instance \
  --zone=us-central1-a \
  [email protected] \
  --scopes=cloud-platform

gcloud compute instances start my-instance --zone=us-central1-a

Tip: Use IAM Recommender to see which permissions a service account actually uses. Run gcloud recommender recommendations list --recommender=google.iam.policy.Recommender --project=my-project to get data-driven suggestions for trimming roles before you commit to them.

Console steps

  1. Open Compute Engine > VM instances and stop the target instance.
  2. Click the instance name, then Edit.
  3. Under Identity and API access, select your dedicated service account.
  4. Choose Set access for each API and enable only what the workload needs, or use a least-privilege service account with full access controlled by IAM.
  5. Save and start the instance.

How to prevent it from happening again

Manual cleanup does not scale. The instances that drift into this state are usually the ones created quickly by hand or by an old template. Catch them at the source.

Terraform: define scopes and service accounts explicitly

resource "google_service_account" "app_vm" {
  account_id   = "app-vm-sa"
  display_name = "App VM service account"
}

resource "google_compute_instance" "app" {
  name         = "app-instance"
  machine_type = "e2-medium"
  zone         = "us-central1-a"

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-12"
    }
  }

  network_interface {
    network = "default"
  }

  service_account {
    email = google_service_account.app_vm.email
    # Scope to only what the workload needs, never the default broad scope
    scopes = [
      "https://www.googleapis.com/auth/devstorage.read_only",
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring.write",
    ]
  }
}

Danger: Never leave the service_account block out of a Terraform instance resource. If you omit it, Compute Engine attaches the default service account with full API scope, which is exactly the misconfiguration this check exists to stop.

Block it in CI with Open Policy Agent

Add a policy-as-code gate so a plan with the full-access scope never reaches production.

package terraform.gcp.vm_scopes

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "google_compute_instance"
  sa := resource.change.after.service_account[_]
  scope := sa.scopes[_]
  scope == "https://www.googleapis.com/auth/cloud-platform"
  not allowed_full_scope_sa[sa.email]
  msg := sprintf("Instance %s uses cloud-platform scope; confirm a least-privilege service account", [resource.address])
}

# Allowlist service accounts that are verified least-privilege
allowed_full_scope_sa := {
  "[email protected]",
}

Org policy as a backstop

You can restrict which service accounts can be attached to VMs using the iam.allowedPolicyMemberDomains and service account usage constraints, and disable automatic creation of the default service account with the iam.automaticIamGrantsForDefaultServiceAccounts org policy. Turning off automatic Editor grants for default service accounts removes the most dangerous combination at the root.

# Prevent default service accounts from getting Editor automatically
gcloud resource-manager org-policies enable-enforce \
  iam.automaticIamGrantsForDefaultServiceAccounts \
  --organization=YOUR_ORG_ID

Best practices

  • One service account per workload. Do not share a single identity across unrelated VMs. Shared identities make the blast radius impossible to reason about.
  • Avoid the default Compute Engine service account. It tends to carry broad project-level roles. Create purpose-built accounts instead.
  • Start from zero and add roles. Grant nothing, then add the minimum roles until the workload functions, rather than starting broad and trimming later.
  • Treat the metadata server as a credential vault. Protect against SSRF, validate outbound requests in your apps, and consider GKE Workload Identity for containerized workloads so pods never touch the node service account.
  • Review scopes during instance template updates. Templates propagate misconfigurations to every instance in a managed instance group, so an old template is a force multiplier.
  • Continuously monitor. Scopes drift as instances get recreated by hand. A scheduled scan that flags cloud-platform scope on VMs catches regressions before an attacker does.

The goal is simple to state and worth repeating: a compromised VM should hand an attacker a tightly limited set of permissions, not the keys to the whole project. Narrow your scopes, dedicate your service accounts, and gate the change in CI so the convenient-but-dangerous default never makes it back.