This check flags any user granted roles/iam.serviceAccountUser or roles/iam.serviceAccountTokenCreator, both of which let a human act as a service account and inherit its permissions. Audit who holds these roles, remove the ones nobody needs, and scope the rest to individual service accounts rather than whole projects.
Service account impersonation is one of the quieter ways privilege escalates inside a GCP project. A user might have read-only access to a project, but if they can also impersonate a service account that has roles/owner, their real permissions are whatever that service account can do. This check looks for exactly that gap: human identities that have been handed the keys to assume a service account's identity.
What this check detects
The iam_saimpersonation check inspects IAM policy bindings across your GCP projects and reports any user that holds one of two roles:
roles/iam.serviceAccountUser— lets the user attach a service account to resources (Compute Engine VMs, Cloud Functions, Dataflow jobs, and so on) and run workloads as that service account.roles/iam.serviceAccountTokenCreator— lets the user mint short-lived OAuth 2.0 access tokens, ID tokens, and signed blobs directly for the service account, no resource attachment required.
Both roles can be granted at the project level (affecting every service account in the project) or on an individual service account. The check surfaces the binding regardless of scope, because both arrangements carry real risk and the project-wide grant is the more dangerous of the two.
Note: Impersonation is not inherently bad. It is the recommended way to avoid long-lived service account keys. The problem is who holds the role and how broadly it is scoped, not the mechanism itself.
Why it matters
The core issue is that impersonation collapses the boundary between a user's own permissions and the service account's permissions. A user with serviceAccountTokenCreator on a privileged service account effectively has that service account's access, even if their direct role bindings look harmless.
A concrete escalation path
Say an engineer has roles/viewer on a project plus roles/iam.serviceAccountTokenCreator at the project level. The project also runs a service account with roles/editor for an automation pipeline. The engineer cannot edit resources directly, but they can run:
gcloud auth print-access-token \
--impersonate-service-account=automation@my-project.iam.gserviceaccount.com
and walk away with a token that has editor rights. From viewer to editor in one command. If any service account in the project carries roles/owner, the same trick yields full project ownership.
Warning: Project-level grants of these roles apply to all service accounts in the project, including ones created later. A binding that looked safe when only low-privilege service accounts existed becomes a liability the moment someone provisions a privileged one.
Why attackers love it
Impersonation chains are a favorite for lateral movement because they often leave a smaller footprint than creating a service account key. Tokens are short-lived and generated through normal IAM APIs, so they blend into legitimate traffic. If an attacker compromises a developer's credentials and that developer has token creator on a privileged service account, the attacker inherits the escalation path without touching a single IAM binding of their own.
How to fix it
1. Find every binding
Start by listing who actually holds these roles. This pulls all bindings for both roles at the project level:
gcloud projects get-iam-policy my-project \
--flatten="bindings[].members" \
--filter="bindings.role:roles/iam.serviceAccountUser OR bindings.role:roles/iam.serviceAccountTokenCreator" \
--format="table(bindings.role, bindings.members)"
Don't stop at the project level. The same roles can be bound on individual service accounts. Check each one you care about:
gcloud iam service-accounts get-iam-policy \
[email protected] \
--format="table(bindings.role, bindings.members)"
2. Remove the bindings nobody needs
For users who shouldn't have impersonation at all, revoke the project-level binding:
Danger: Removing these roles can break running workloads if a human account is being used to attach service accounts to VMs or trigger pipelines. Confirm the member is a real user and not a deployment identity before revoking, and communicate with the owner first.
gcloud projects remove-iam-policy-binding my-project \
--member="user:[email protected]" \
--role="roles/iam.serviceAccountTokenCreator"
3. Re-scope the bindings you keep
For users who legitimately need impersonation, grant the role on the specific service account instead of the whole project. This is the single most important fix because it eliminates blanket access.
# First, remove the project-wide grant
gcloud projects remove-iam-policy-binding my-project \
--member="user:[email protected]" \
--role="roles/iam.serviceAccountUser"
# Then grant it only on the service account they actually use
gcloud iam service-accounts add-iam-policy-binding \
[email protected] \
--member="user:[email protected]" \
--role="roles/iam.serviceAccountUser"
4. Prefer groups over individual users
Granting to a Google Group rather than individual users makes the binding stable as people join and leave. You manage membership in one place and the IAM policy stays clean.
gcloud iam service-accounts add-iam-policy-binding \
[email protected] \
--member="group:[email protected]" \
--role="roles/iam.serviceAccountUser"
Tip: If you need to allow impersonation but with extra guardrails, attach an IAM Condition so the role only applies during business hours or to specific resources. Conditions are evaluated at access time and cost nothing to add.
How to prevent it from coming back
Manual cleanup fixes today's state. Policy and CI gates keep it fixed.
Org policy to limit who can be granted impersonation
Use the iam.allowedPolicyMemberDomains constraint to block external members entirely, then layer custom organization policies or IAM deny policies to restrict the high-risk roles. An IAM deny policy can stop these roles from being granted at the project level org-wide:
{
"displayName": "Deny project-level SA impersonation to users",
"rules": [
{
"denyRule": {
"deniedPrincipals": ["principalSet://goog/public:all"],
"exceptionPrincipals": [
"principalSet://iam.googleapis.com/groups/[email protected]"
],
"deniedPermissions": [
"iam.googleapis.com/serviceAccounts.getAccessToken",
"iam.googleapis.com/serviceAccounts.actAs"
]
}
}
]
}
Catch it in Terraform before it ships
If you manage IAM in Terraform, scope bindings to the resource, not the project, and gate them with policy-as-code. Here is the right pattern:
resource "google_service_account_iam_member" "deploy_impersonation" {
service_account_id = google_service_account.deploy.name
role = "roles/iam.serviceAccountUser"
member = "group:[email protected]"
}
Then block the dangerous shape with an OPA / Conftest rule in CI:
package gcp.iam
deny[msg] {
resource := input.resource_changes[_]
resource.type == "google_project_iam_member"
role := resource.change.after.role
role == "roles/iam.serviceAccountTokenCreator"
msg := sprintf("Project-level token creator binding is not allowed: %v", [resource.address])
}
Tip: Run Lensix on a schedule so that drift introduced through the console or ad hoc gcloud commands gets flagged even when it bypasses your Terraform pipeline. CI gates only cover what goes through CI.
Best practices
- Grant on the service account, never the project. Project-level impersonation roles are the most common source of accidental escalation.
- Use impersonation instead of keys. Short-lived impersonated tokens are far safer than downloaded JSON keys, so the goal is to scope impersonation tightly, not eliminate it.
- Map the privilege of each service account first. A token creator binding on a low-privilege service account is a minor finding. The same binding on an owner-level service account is critical. Triage by what the service account can do.
- Bind to groups, not individuals. It keeps IAM policies readable and removes the cleanup burden when people change teams.
- Audit token generation in logs. Enable Cloud Audit Logs for IAM and alert on
GenerateAccessTokencalls from unexpected principals. - Review quarterly. Impersonation grants accumulate. A recurring access review catches the ones that outlived their purpose.
If you remember one thing: a user's effective permissions include everything they can impersonate. Audit impersonation with the same seriousness you audit direct role grants.

