Back to blog
Best PracticesCloud SecurityGCPIdentity & Access

Default Service Account Has IAM Role: Locking Down GCP's Riskiest Default

Learn why a default Compute Engine or App Engine service account with an IAM role is a project-wide breach risk, and how to remediate and prevent it in GCP.

TL;DR

This check flags any IAM role assigned to GCP's default Compute Engine or App Engine service account. Those accounts carry broad default permissions and are attached to workloads automatically, so a single compromised VM can become a project-wide breach. Fix it by creating dedicated, least-privilege service accounts per workload and stripping roles from the defaults.

Every new GCP project quietly ships with a couple of service accounts you never asked for. The Compute Engine default service account and the App Engine default service account exist the moment you enable those APIs, and Google attaches them to your VMs and App Engine apps unless you tell it otherwise. They are convenient, which is exactly why they are dangerous. This Lensix check, iam_defaultsaassignment, looks for any IAM role binding granted to one of these default accounts and flags it so you can move toward dedicated, scoped identities instead.


What this check detects

The check inspects your project's IAM policy and identifies role bindings where the member is one of the default service accounts:

If either of these accounts appears as a member in any IAM binding, the check reports a finding. In practice it almost always will, because GCP grants the Compute Engine default service account the roles/editor role on the project at creation time.

Note: The roles/editor basic role allows read and write access to nearly every resource in a project, including creating and deleting most services. It is one of three legacy "basic roles" (owner, editor, viewer) that predate IAM's granular predefined roles and are far too broad for almost any real workload.


Why it matters

The risk here is not abstract. The default service account is attached to compute resources, and by default those resources can request OAuth tokens for it through the metadata server. That combination is what turns a routine workload compromise into a full project takeover.

The attack path

  1. An attacker gets code execution on a Compute Engine VM, through an SSRF bug, a vulnerable dependency, or an exposed application.
  2. They query the instance metadata server at http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token to grab an access token.
  3. That token belongs to the default service account, which holds roles/editor on the entire project.
  4. The attacker now reads from storage buckets, spins up crypto-mining instances, modifies firewall rules, and pivots to other resources, all with legitimate credentials.

SSRF alone is enough to trigger this. The attacker does not even need shell access if your application can be tricked into fetching the metadata URL on their behalf.

Warning: The default access scope on older VMs further restricts which APIs the token can reach, but scopes are a coarse, deprecated control layer. Never rely on access scopes as your security boundary. They were designed before IAM matured and can be widened by anyone who can edit the instance.

From a blast radius perspective, this means every VM in a project shares one over-privileged identity. There is no separation between your batch processing box and your customer-facing API. Compromise one, compromise the project.


How to fix it

The goal is simple to state: stop using default service accounts for workloads, and remove the broad roles they carry. Here is how to get there without breaking running services.

Step 1: Find what the default account is actually used for

Before stripping anything, check which resources currently run as the default account so you do not pull the rug out from under a live workload.

gcloud compute instances list \
  --format="table(name, zone, serviceAccounts[0].email)" \
  --project=YOUR_PROJECT_ID

Review the IAM bindings the default account holds:

gcloud projects get-iam-policy YOUR_PROJECT_ID \
  --flatten="bindings[].members" \
  --filter="bindings.members:*[email protected]" \
  --format="table(bindings.role)"

Step 2: Create a dedicated, least-privilege service account

gcloud iam service-accounts create my-app-sa \
  --display-name="My App Workload SA" \
  --project=YOUR_PROJECT_ID

Grant it only the specific predefined roles the workload needs. For example, an app that only reads from a storage bucket and writes logs:

gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
  --member="serviceAccount:my-app-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/storage.objectViewer"

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

Step 3: Reattach workloads to the new account

For a Compute Engine instance, set the service account and request fresh tokens. The instance must be stopped to change its service account.

Danger: The command below stops a running instance, which causes downtime for anything served from it. Drain traffic, fail over, or run this during a maintenance window before executing in production.

gcloud compute instances stop INSTANCE_NAME --zone=ZONE

gcloud compute instances set-service-account INSTANCE_NAME \
  --zone=ZONE \
  --service-account=my-app-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com \
  --scopes=cloud-platform

gcloud compute instances start INSTANCE_NAME --zone=ZONE

Step 4: Remove roles from the default service account

Once nothing depends on it, revoke the roles/editor binding (and any others) from the default account.

Danger: Confirm no live workloads, scheduled jobs, or pipelines still authenticate as the default service account before removing its roles. Pulling roles/editor from an account something quietly relies on will cause permission-denied failures across that workload.

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

You can also disable the default account entirely if nothing needs it:

gcloud iam service-accounts disable \
  [email protected] \
  --project=YOUR_PROJECT_ID

Tip: GCP has an org policy that stops the default editor grant from ever being created. Enforce iam.automaticIamGrantsForDefaultServiceAccounts as a deny constraint at the org or folder level and new projects will spin up without the broad default binding in the first place. That fixes the problem at the source instead of cleaning it up per project.


How to prevent it from happening again

Manual cleanup does not scale across dozens of projects. Bake the controls into your platform so new projects are born compliant.

Enforce the org policy constraint

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

Define dedicated service accounts in Terraform

Provision workload identities as code so there is no temptation to fall back on the default. Notice the explicit, narrow role assignment.

resource "google_service_account" "app" {
  account_id   = "my-app-sa"
  display_name = "My App Workload SA"
  project      = var.project_id
}

resource "google_project_iam_member" "app_storage" {
  project = var.project_id
  role    = "roles/storage.objectViewer"
  member  = "serviceAccount:${google_service_account.app.email}"
}

resource "google_compute_instance" "app" {
  name         = "app-vm"
  machine_type = "e2-medium"
  zone         = var.zone

  service_account {
    email  = google_service_account.app.email
    scopes = ["cloud-platform"]
  }
  # ... boot disk, network, etc.
}

Add a policy-as-code gate in CI/CD

Block merges that attach a default service account or grant it a role. A Conftest/OPA Rego policy run against your Terraform plan catches it before apply:

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "google_project_iam_member"
  contains(resource.change.after.member, "[email protected]")
  msg := sprintf("IAM role granted to default Compute Engine SA: %s", [resource.address])
}

Tip: Pair the CI gate with a scheduled Lensix scan. The pipeline catches new infrastructure before it ships, and the recurring scan catches drift from console clicks and emergency changes that bypassed the pipeline.


Best practices

  • One service account per workload. Each distinct application or job gets its own identity. This keeps the blast radius of any compromise to a single workload instead of the whole project.
  • Predefined roles over basic roles. Avoid roles/owner, roles/editor, and roles/viewer for workloads entirely. Use granular predefined roles, or custom roles when those are still too broad.
  • Disable the metadata token exposure where you can. If a workload does not need to call Google APIs, do not attach a service account with broad scopes. For GKE, use Workload Identity instead of node service accounts.
  • Treat default service accounts as something to remove, not configure. They are a starting point GCP hands you, not a recommendation to use them.
  • Audit regularly. Service account sprawl and role creep happen quietly. Recheck bindings on a schedule and alert on any new grant to a default account.

The convenient default and the secure default are rarely the same thing. The Compute Engine default service account with editor on the project is convenient. A scoped, dedicated account per workload is secure. Choose the second one before an attacker chooses the first for you.

Clearing this Lensix finding is not just about silencing an alert. It is about removing one of the most reliable privilege-escalation paths in GCP, the one attackers reach for first after landing on a VM. Fix it once with org policy and Terraform, gate it in CI, and the problem stops recurring.