SetIamPolicy abuse: finding owner grants and outsiders
Read SetIamPolicy audit entries like an investigator: bindingDeltas, Owner granted to Gmail accounts, unknown domains, allUsers, Token Creator, org policies.
TL;DR. Every change to an IAM policy is an Admin Activity entry, and the useful part is not the full policy in the request but the diff: serviceData.policyDelta.bindingDeltas (or metadata.policyDelta, metadata.datasetChange for BigQuery), with action, role and member per change. Look for ADD of roles/owner, roles/editor or IAM admin roles, of consumer accounts (@gmail.com), of domains that never appear among your callers, of allUsers, and of the Token Creator / Service Account User roles.
Once an attacker has a credential with setIamPolicy permission, the first thing they usually do is give themselves a durable identity they control. In MITRE ATT&CK terms this is T1098.003, Additional Cloud Roles. It is also the easiest step to spot, because Admin Activity logs are always on and kept 400 days.
What a SetIamPolicy entry contains
gcloud projects add-iam-policy-binding looks like a small change, but the API is read-modify-write: the client calls GetIamPolicy, edits the policy locally and sends the whole thing back with SetIamPolicy. The request therefore contains the entire new policy. Reading it tells you the result, not what changed.
The change is in the policy delta:
"serviceData": {
"@type": "type.googleapis.com/google.iam.v1.logging.AuditData",
"policyDelta": {
"bindingDeltas": [
{ "action": "ADD", "role": "roles/owner", "member": "user:someone@gmail.com" }
]
}
}
Depending on the service and API version, the same structure appears under protoPayload.metadata.policyDelta, or, for BigQuery datasets and tables, under metadata.datasetChange.bindingDeltas / metadata.tableChange.bindingDeltas (BigQuery audit logs).
The method name also varies by resource:
| Resource | Typical methodName |
|---|---|
| Project, folder, organization | SetIamPolicy (service cloudresourcemanager.googleapis.com) |
| Cloud Storage bucket | storage.setIamPermissions |
| Service account | google.iam.admin.v1.SetIAMPolicy |
| Compute image, snapshot, disk | v1.compute.images.setIamPolicy, …snapshots.setIamPolicy |
| BigQuery dataset or table | Dataset update or table IAM methods; the delta sits in metadata.datasetChange / metadata.tableChange |
So search on the delta, not only on one method name.
The five grants that matter
1. Owner, Editor and IAM administration roles
roles/owner and roles/editor are the basic roles with (near) full control (roles overview). roles/resourcemanager.projectIamAdmin, roles/resourcemanager.organizationAdmin and roles/iam.securityAdmin are just as dangerous because they allow granting anything else. Any ADD of these during an incident window is a lead.
2. Consumer accounts
A member ending with @gmail.com or @googlemail.com is a personal Google account. Attackers like them: free, anonymous, and they survive the rotation of every credential in your organisation. ADD roles/owner user:x@gmail.com is, unless it is a documented break-glass account, a takeover.
3. Unknown domains
Less obvious: a user: or group: member from a domain that never appears as a caller in your logs. It can be a contractor; it can be the attacker's own Workspace tenant. The domain restricted sharing constraint (iam.allowedPolicyMemberDomains) prevents both.
4. allUsers and allAuthenticatedUsers
These special principals make a resource public. On a bucket, it means anyone on the Internet can read the objects. See allUsers and the exfiltration article.
5. Impersonation roles
roles/iam.serviceAccountTokenCreator, roles/iam.serviceAccountUser, roles/iam.serviceAccountKeyAdmin and roles/iam.workloadIdentityUser let the member act as service accounts. They are quieter than Owner and often go unnoticed. See service account impersonation.
Changes around the policy
Two more Admin Activity events belong in the same review:
- Organization policy changes (
google.cloud.orgpolicy.v2.OrgPolicy.CreatePolicy,UpdatePolicy,DeletePolicy, or the olderSetOrgPolicy/ClearOrgPolicy). An attacker who relaxesiam.allowedPolicyMemberDomainsbefore adding a Gmail account, oriam.disableServiceAccountKeyCreationbefore creating keys, is preparing the next step (T1484). - Custom roles (
google.iam.admin.v1.CreateRole,UpdateRole,UndeleteRole). A role called "Viewer (legacy)" can containiam.serviceAccountKeys.create. Rhino Security Labs' research on GCP privilege escalation listsiam.roles.updateamong the escalation paths.
Audit configuration changes (auditConfigDeltas) also travel through SetIamPolicy; they are covered in defense evasion.
Investigating a suspicious grant
For each suspicious delta, answer:
- Who made the call?
authenticationInfo.principalEmail, andserviceAccountKeyNameorserviceAccountDelegationInfoif present. A grant made by a CI service account with a key, from a VPS, is not a human mistake. - From where?
requestMetadata.callerIpand user agent. Compare with the principal's usual IPs. - What happened next? Search for the new member as
principalEmail. An account that receives Owner and then acts from the same IP within minutes closes the loop. - Is it still there? The latest
SetIamPolicyresponses, or a currentgcloud projects get-iam-policy, tell you whether the binding was removed. - What else changed? Look for
REMOVEdeltas too: attackers sometimes remove legitimate owners.
A Logs Explorer query that catches most project-level changes:
logName:"cloudaudit.googleapis.com%2Factivity"
protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD"
What the analyzer flags
The rules in the analyzer evaluate every delta separately, whatever the resource:
| Rule | Severity | Condition on a delta |
|---|---|---|
owner_to_consumer | Critical | ADD of roles/owner or roles/editor to a @gmail.com / @googlemail.com member |
public_access_granted | Critical | ADD of allUsers or allAuthenticatedUsers |
owner_editor_granted | High | ADD of Owner, Editor, Organization Admin, Project IAM Admin or Security Admin |
consumer_account_granted | High | ADD of any consumer account |
token_creator_granted | High | ADD of Token Creator, Service Account User, Key Admin or Workload Identity User |
new_external_domain (analytic) | Medium | ADD of a member from a domain never seen among the callers |
org_policy_changed | Medium | Organization policy created, updated or deleted |
custom_role_changed | Low | Custom role created, updated or undeleted |
Targets are shown as readable deltas (ADD roles/owner user:…), and the Entities tab marks consumer accounts as "personal Gmail".
Remediation
Remove the bindings, then check the whole hierarchy (project, folders, organization) for other additions; enforce domain restricted sharing; review custom roles; and restore any organization policy that was relaxed. Google's compromised credentials guide covers the identity side.
Frequently asked questions
How do I see who granted Owner on a Google Cloud project?
Query Admin Activity audit logs for protoPayload.methodName="SetIamPolicy" and read protoPayload.serviceData.policyDelta.bindingDeltas: each delta has an action (ADD or REMOVE), a role and a member. The caller is protoPayload.authenticationInfo.principalEmail.
Is a gmail.com account in an IAM policy always malicious?
No, some small organisations use personal accounts, and break-glass accounts exist. But a consumer account receiving roles/owner during an incident is the classic takeover move, and the iam.allowedPolicyMemberDomains policy exists to prevent it.
Further reading
- IAM roles overview — Google Cloud documentation.
- Restricting identities by domain — Google Cloud documentation.
- Privilege Escalation in Google Cloud Platform – Part 1 (IAM) — Rhino Security Labs.
- Leaked GCP service account key: how to investigate it.