Back to blog
Best PracticesCloud SecurityCompute & ContainersGCPIdentity & Access

VM Uses Default Service Account With Full Access (GCP)

Learn why Compute Engine VMs on the default service account with cloud-platform scope are a privilege escalation risk, and how to fix and prevent it.

TL;DR

This check flags Compute Engine VMs running on the default service account with the cloud-platform scope, which grants the VM full access to every Google Cloud API. Replace it with a dedicated service account scoped to only the permissions the workload needs.

When you spin up a Compute Engine instance in the console without thinking too hard about identity, GCP quietly does two things on your behalf. It attaches the default Compute Engine service account, and on certain configurations it grants that account broad API access scopes. The combination is convenient, and it is also one of the most common privilege escalation paths in a GCP environment.

This Lensix check, compute_defaultserviceaccount, looks for VMs that are running as the default service account and have the https://www.googleapis.com/auth/cloud-platform scope. That scope is the wildcard. It means the VM can call any Google Cloud API the service account has IAM permission for.


What this check detects

Every project comes with a default Compute Engine service account named like this:

[email protected]

Two things have to be true for a VM to fail this check:

  1. The instance is configured to run as the default Compute Engine service account.
  2. The instance has the cloud-platform access scope, which permits calls to the full set of Google Cloud APIs.

Note: Access scopes are a legacy authorization layer that sits in front of IAM. Even if the service account has narrow IAM roles, a broad scope removes one of the guardrails. Google now recommends ignoring scopes in favor of IAM, but the cloud-platform scope plus the default account is a known bad pairing because the default account often carries the broad Editor role on the project.

That last point is the kicker. In older projects, the default Compute Engine service account is automatically granted the Editor role at the project level. Editor can read, write, and delete the vast majority of resources in the project. So a VM with the default account and the cloud-platform scope effectively has Editor over your whole project.


Why it matters

The risk here is not the VM itself. It is what happens when someone or something gains code execution on that VM.

Compute Engine instances expose credentials through the metadata server at 169.254.169.254. Any process on the box, including an attacker who exploited your web app, can pull a live access token for the attached service account:

curl -s -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"

If that token belongs to the default account with Editor and a cloud-platform scope, the attacker now has a token that can do almost anything in the project. They can read every Cloud Storage bucket, dump Cloud SQL data through new instances, create their own service account keys, spin up crypto mining VMs, or pivot to other projects if the account has cross-project bindings.

Danger: This is one of the most common real-world GCP breach chains: SSRF or RCE in an app, token theft from the metadata server, then lateral movement using overly broad VM credentials. A single exposed endpoint can become a full project compromise.

The blast radius matters for compliance too. CIS Google Cloud Foundations Benchmark calls this out directly (control 4.1 and 4.2), and auditors will flag default service account usage as a least-privilege violation.


How to fix it

The fix is to stop using the default account on the VM and attach a dedicated, narrowly scoped service account instead. Service accounts cannot be changed while an instance is running, so you stop the VM, swap the account, and start it again.

Step 1: Create a dedicated service account

gcloud iam service-accounts create my-app-sa \
  --display-name="Service account for my-app VMs" \
  --project=MY_PROJECT

Step 2: Grant only the roles the workload needs

Resist the urge to grant Editor. Figure out what the workload actually calls and bind those roles. For a VM that reads from one bucket and writes logs, that might be:

gcloud projects add-iam-policy-binding MY_PROJECT \
  --member="serviceAccount:my-app-sa@MY_PROJECT.iam.gserviceaccount.com" \
  --role="roles/logging.logWriter"

gsutil iam ch \
  serviceAccount:my-app-sa@MY_PROJECT.iam.gserviceaccount.com:roles/storage.objectViewer \
  gs://my-app-input-bucket

Step 3: Stop the VM and attach the new account

Warning: Stopping the instance causes downtime. Drain traffic from the VM first if it is behind a load balancer, or do this during a maintenance window. For zero-downtime, roll out a new instance template and replace instances in a managed instance group instead.

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

gcloud compute instances set-service-account my-instance \
  --zone=us-central1-a \
  --service-account=my-app-sa@MY_PROJECT.iam.gserviceaccount.com \
  --scopes=cloud-platform

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

Note: Keeping --scopes=cloud-platform on a dedicated account is fine and is in fact Google's recommended pattern. With a dedicated account, IAM roles control what the token can do, so scopes become redundant and the broad scope no longer maps to broad permissions. The danger only exists when broad scope meets the over-privileged default account.

Doing it in Terraform

If you manage instances as code, set the service account block explicitly so the default account is never used:

resource "google_service_account" "app" {
  account_id   = "my-app-sa"
  display_name = "Service account for my-app VMs"
}

resource "google_compute_instance" "app" {
  name         = "my-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.email
    scopes = ["cloud-platform"]
  }
}

Tip: If you never plan to run workloads on the default Compute Engine service account, disable it entirely. A disabled default account means no new VM can accidentally inherit it. Run gcloud iam service-accounts disable [email protected] after confirming nothing depends on it.

Strip the Editor role from the default account

Even after migrating your VMs, the default account may still carry Editor. Remove it so any future accidental usage is far less dangerous:

gcloud projects remove-iam-policy-binding MY_PROJECT \
  --member="serviceAccount:[email protected]" \
  --role="roles/editor"

Danger: Confirm no running workloads depend on the default account before removing its Editor role. Removing it can break VMs, Cloud Build, or other services that still rely on it. Audit with gcloud asset search-all-iam-policies first.


How to prevent it from happening again

One-off fixes drift back. Bake the rule into the platform so a bad config can never reach production.

Disable automatic IAM grants for default accounts

GCP has an org policy that stops new default service accounts from getting automatic IAM roles like Editor. Enforce it at the organization level:

gcloud resource-manager org-policies enable-enforce \
  iam.automaticIamGrantsForDefaultServiceAccounts \
  --organization=ORG_ID

Block the default account on instances with org policy

The compute.disableDefaultServiceAccount wave is handled through usage constraints. Combine it with restricting the cloud-platform scope using the compute.vmExternalIpAccess family of controls and a custom org policy that requires a non-default service account. At minimum, enforce that VMs cannot launch without an explicit service account in your IaC review.

Gate it in CI/CD with policy-as-code

Catch the misconfiguration before terraform apply. Here is an OPA/Conftest rule that fails any instance using the default account pattern:

package compute

deny[msg] {
  resource := input.resource.google_compute_instance[name]
  sa := resource.service_account[_]
  endswith(sa.email, "[email protected]")
  msg := sprintf("instance '%s' uses the default Compute Engine service account", [name])
}

Tip: Wire this into your pull request pipeline so the check runs on every plan. Lensix continuously scans live resources for the same condition, which covers the gap between IaC and reality, the manually created VM someone spun up in the console during an incident.


Best practices

  • One service account per workload. Distinct accounts mean a compromise is contained to that one workload's permissions, and you get clean audit trails in Cloud Logging.
  • Least privilege, always. Start from zero roles and add only what the application proves it needs. Use the IAM recommender to trim unused permissions.
  • Never grant Editor or Owner to a service account. These primitive roles are too broad for any automated workload. Use predefined or custom roles.
  • Lock down the metadata server. Treat metadata server access as sensitive. Use a proxy or firewall egress rules to limit which processes can reach 169.254.169.254 where feasible.
  • Prefer Workload Identity for GKE. If your workloads run on Kubernetes, use Workload Identity instead of node service accounts so each pod gets its own scoped identity.
  • Audit regularly. Default account usage creeps back in over time as teams create resources quickly. A continuous check beats a quarterly review.

The default service account exists to make getting started easy, not to run production. Treat every VM identity as a deliberate decision, scope it tight, and the metadata server stops being a liability.