This check flags GCP service accounts bound to roles/owner, roles/editor, or any *Admin role. These accounts are non-human, often have keys floating around, and a single leaked credential gives an attacker project-wide control. Fix it by replacing the broad role with a narrow set of predefined or custom roles scoped to what the account actually does.
Service accounts are the workhorses of any GCP project. They run your Cloud Run services, your GKE workloads, your CI pipelines, and your scheduled jobs. Unlike a human user who logs in with a password and MFA, a service account authenticates with a key or a token, and those credentials tend to leak in ways humans never do: committed to Git, baked into container images, dumped in CI logs, or passed around in Slack.
So when one of those non-human identities holds Owner, Editor, or an Admin role, you have combined the worst of both worlds: a credential that is easy to expose and a permission set that can do almost anything. That is exactly what the iam_privilegedsa check looks for.
What this check detects
The check inspects IAM policy bindings across your project and identifies any service account that is granted one of the following:
roles/owner— full control over the project, including IAM policy modification and deletion.roles/editor— read and write access to almost every resource in the project. This is the default role attached to the legacy Compute Engine and App Engine default service accounts.- Any admin role — for example
roles/storage.admin,roles/iam.serviceAccountAdmin,roles/compute.admin, orroles/resourcemanager.projectIamAdmin.
A service account is anything with an email ending in .gserviceaccount.com. The check looks at who the role is bound to, not who created it, so default service accounts that Google auto-provisions are flagged too.
Note: The Compute Engine default service account ([email protected]) is granted the Editor role automatically when the Compute Engine API is enabled. Every VM that uses the default service account inherits that power unless you override it. This is the single most common way this check trips.
Why it matters
The Editor role alone covers thousands of individual permissions. Owner adds the ability to grant roles to anyone, including granting Owner to an external account. Here is what that looks like when it goes wrong.
Credential leak turns into full takeover
A developer exports a JSON key for a service account so a local script can talk to BigQuery. The key gets committed to a private repo. Months later the repo is made public, or a contractor's laptop is compromised. If that service account had a narrow roles/bigquery.dataViewer grant, the blast radius is one dataset. If it had roles/editor, the attacker can now spin up crypto-mining VMs, read every storage bucket, modify firewall rules, and exfiltrate your databases.
Privilege escalation through the account itself
A service account with roles/iam.serviceAccountAdmin or roles/owner can mint keys for other service accounts or grant itself additional roles. An attacker who lands on a single over-privileged account can pivot to every other identity in the project, which makes containment during an incident much harder.
Lateral movement from a workload
If a public-facing Cloud Run service or a GKE pod runs as an Editor service account and an attacker finds an SSRF or RCE bug in your application, they can hit the metadata endpoint, grab the account's access token, and use those project-wide permissions without ever needing the JSON key.
Warning: Service account access tokens retrieved from the metadata server are valid for up to an hour and cannot be revoked individually. If an Editor account token leaks, your only real option is to remove the account's permissions and rotate keys, then wait out or invalidate active sessions.
How to fix it
The goal is to replace the broad role with the smallest set of roles the account actually needs. The process is the same regardless of which privileged role is attached.
Step 1: Find what the account is actually doing
Before you yank permissions, figure out what the account needs. Use IAM Recommender, which analyzes 90 days of usage and suggests a tighter role set.
gcloud recommender recommendations list \
--project=MY_PROJECT \
--location=global \
--recommender=google.iam.policy.Recommender \
--format=json
You can also inspect recent permission usage through Cloud Audit Logs to confirm which APIs the account calls.
Step 2: Identify the current binding
gcloud projects get-iam-policy MY_PROJECT \
--flatten="bindings[].members" \
--filter="bindings.members:serviceAccount AND bindings.role:(roles/owner OR roles/editor)" \
--format="table(bindings.role, bindings.members)"
Step 3: Grant the narrow roles first
Add the scoped roles before you remove the broad one so the workload never loses access mid-change.
gcloud projects add-iam-policy-binding MY_PROJECT \
--member="serviceAccount:my-sa@MY_PROJECT.iam.gserviceaccount.com" \
--role="roles/bigquery.dataViewer"
gcloud projects add-iam-policy-binding MY_PROJECT \
--member="serviceAccount:my-sa@MY_PROJECT.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
Step 4: Remove the privileged role
Danger: Removing a role from a service account that a running workload depends on will break that workload immediately. Confirm the replacement roles are in place and tested in a non-production project first. Pay special attention to the Compute Engine default service account, since pulling Editor from it can break VMs, GKE nodes, and Cloud Build jobs that quietly relied on it.
gcloud projects remove-iam-policy-binding MY_PROJECT \
--member="serviceAccount:my-sa@MY_PROJECT.iam.gserviceaccount.com" \
--role="roles/editor"
Fixing it in Terraform
If you manage IAM as code, the fix lives in your config. Replace the broad binding with scoped ones.
# Before: too broad
resource "google_project_iam_member" "sa_editor" {
project = var.project_id
role = "roles/editor"
member = "serviceAccount:${google_service_account.app.email}"
}
# After: scoped to what the workload needs
resource "google_project_iam_member" "sa_bq_viewer" {
project = var.project_id
role = "roles/bigquery.dataViewer"
member = "serviceAccount:${google_service_account.app.email}"
}
resource "google_project_iam_member" "sa_storage_viewer" {
project = var.project_id
role = "roles/storage.objectViewer"
member = "serviceAccount:${google_service_account.app.email}"
}
Dealing with the default service account
For the Compute Engine and App Engine default service accounts, the better long-term move is to stop using them entirely. Create a dedicated, scoped service account per workload, and either disable the default account or remove its Editor binding.
# Create a purpose-built account for a workload
gcloud iam service-accounts create app-worker \
--display-name="App worker - scoped" \
--project=MY_PROJECT
# Attach it to a VM instead of the default
gcloud compute instances set-service-account my-instance \
--service-account="app-worker@MY_PROJECT.iam.gserviceaccount.com" \
--scopes=cloud-platform \
--zone=us-central1-a
Tip: When a service account needs custom permissions that no predefined role matches cleanly, build a custom role with gcloud iam roles create listing only the permissions IAM Recommender surfaced. It is more work up front but it gives you a stable, auditable least-privilege grant you can reuse.
How to prevent it from happening again
Catching this once is fine. Stopping it from creeping back in is what actually moves the needle.
Block it in CI with policy-as-code
If you provision IAM through Terraform, scan plans before they apply. Conftest with an OPA policy can fail a build that grants a privileged role to a service account.
package gcp.iam
deny[msg] {
resource := input.resource_changes[_]
resource.type == "google_project_iam_member"
role := resource.change.after.role
member := resource.change.after.member
startswith(member, "serviceAccount:")
privileged := {"roles/owner", "roles/editor"}
privileged[role]
msg := sprintf("Service account %v must not be granted %v", [member, role])
}
Wire it into the pipeline so the gate runs on every plan:
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
conftest test tfplan.json --policy policy/
Enforce with Organization Policy
The iam.allowedPolicyMemberDomains and IAM deny policies let you set guardrails at the org or folder level that no individual project owner can override. You can write a deny policy that blocks granting roles/owner to any service account principal.
Note: IAM deny policies are evaluated before allow policies, so a deny rule wins even if someone later adds an allow binding. They are the strongest preventive control GCP offers for this exact problem.
Continuous detection
Policy gates only cover changes that flow through your pipelines. Someone with console access can always click their way to a bad binding. Lensix runs the iam_privilegedsa check continuously so a privileged service account binding gets surfaced no matter how it was created, including click-ops changes and resources provisioned outside Terraform.
Best practices
- One service account per workload. Shared accounts force you to grant the union of every workload's permissions, which inflates privilege. Dedicated accounts keep the blast radius small.
- Never use Owner or Editor for automation. These roles exist for humans doing administrative work, not for running code. Always prefer predefined roles, then custom roles.
- Kill long-lived keys. Use Workload Identity Federation for external CI, and Workload Identity for GKE, so you never export a JSON key at all. A leaked key you never created cannot hurt you.
- Run IAM Recommender on a schedule. Usage drifts over time. Permissions granted for a feature that shipped two years ago often sit unused and become pure risk.
- Audit default service accounts on day one. Disable or scope down the Compute Engine and App Engine defaults in every new project before any workload starts relying on them.
- Treat IAM bindings as code. Review every role grant the same way you review application code, with a pull request and a second set of eyes.
The principle underneath all of this is simple: a service account should be able to do exactly what its workload needs and nothing more. Owner, Editor, and admin roles are the opposite of that, and on a non-human identity they are a standing invitation to whoever finds the credential first.

