Find who granted an IAM role and when from Cloud Audit Logs
For developers and responders who need a fast, defensible answer to "who granted this role, and when?" in Google Cloud. This runbook shows the exact Cloud Audit Logs queries, `gcloud` commands, and log fields to use, plus what to do when the answer is missing because of scope, retention, or inherited policy.
TL;DR — To answer "who granted this role, and when" in Google Cloud, query Cloud Audit Logs for IAM policy changes (
SetIamPolicy) at the right resource scope, then readprotoPayload.authenticationInfo.principalEmail,timestamp, and the policy delta inprotoPayload.serviceData.policyDelta.bindingDeltas. The most common miss is searching only the project when the grant happened on a folder, organization, or the role is inherited from an ancestor. Reading time: ~6 min
The scenario
It is 3:40 PM on a Tuesday and someone notices a service account suddenly has roles/storage.admin on a production bucket. Nobody remembers approving it, the Terraform PRs look unrelated, and you need an answer before the security review starts in 20 minutes. You open Logs Explorer, search the project, and get nothing useful except a wall of unrelated admin activity. The real task is not "find any IAM log"; it is proving who added the binding, when it happened, and whether the access came from the bucket, the project, or an inherited parent policy.
Symptoms
- A user, group, or service account currently has a role, but ownership is unclear.
- You need an auditable answer with actor and timestamp, not a guess from Terraform history.
- Logs Explorer searches return no results for obvious queries like
SetIamPolicy. - The role appears on the effective policy, but not on the resource policy you first checked.
- Typical relevant log method names include:
protoPayload.methodName="SetIamPolicy"
protoPayload.methodName="google.iam.admin.v1.CreateRole"
protoPayload.methodName="google.iam.admin.v1.UpdateRole"
- Useful fields in matching entries look like:
{
"timestamp": "2026-08-05T14:12:33.481Z",
"protoPayload": {
"authenticationInfo": {
"principalEmail": "alice@example.com"
},
"methodName": "SetIamPolicy",
"resourceName": "projects/_/buckets/prod-artifacts",
"serviceData": {
"policyDelta": {
"bindingDeltas": [
{
"action": "ADD",
"role": "roles/storage.admin",
"member": "serviceAccount:deploy-bot@myproj.iam.gserviceaccount.com"
}
]
}
}
}
}
- When the wrong scope is searched, you often see no error at all — just zero results.
- When retention is the problem, older events are simply absent unless routed to a longer-lived log bucket or sink.
Likely causes
| Cause | How common | Quick check |
|---|---|---|
| You searched the wrong resource scope; the grant was on an org, folder, bucket, service account, or other child resource | Very common | `gcloud projects get-iam-policy PROJECT_ID --format=json |
| The role is inherited from a parent policy, not granted directly on the resource you inspected | Very common | In your provider's console, open the resource IAM page and compare direct vs inherited bindings if shown; otherwise run gcloud asset analyze-iam-policy --full-resource-name=//cloudresourcemanager.googleapis.com/projects/PROJECT_NUMBER --identity=MEMBER |
You queried the wrong log shape; the actor is in audit logs under SetIamPolicy and the change details are in policyDelta | Common | gcloud logging read 'protoPayload.methodName="SetIamPolicy"' --limit=5 --format=json |
| The event is older than retention in the searched log bucket/project | Common | gcloud logging buckets describe _Default --location=global --project=PROJECT_ID |
| The change was made by automation or impersonation, so the visible actor is a service account unless you inspect delegation fields | Occasional | gcloud logging read 'protoPayload.methodName="SetIamPolicy" AND protoPayload.authenticationInfo.serviceAccountDelegationInfo:*' --limit=10 --format=json |
| The grant came from IaC or a group membership change, not a direct member binding on the resource | Occasional | `gcloud projects get-iam-policy PROJECT_ID --flatten="bindings[].members" --format='table(bindings.role,bindings.members)' |
Step-by-step diagnosis
- Check whether the access is direct or inherited.
gcloud projects get-iam-policy PROJECT_ID --format=json > /tmp/project-iam.json
jq '.bindings[] | select(.role=="roles/storage.admin")' /tmp/project-iam.json
If the member is present here, the grant exists at the project level. If not, the access may be on the target resource itself or inherited from a folder/org. Jump to the fix section for the matching cause after you identify the level.
- If the role is on a specific resource, inspect that resource's IAM policy directly.
gsutil iam get gs://prod-artifacts | jq '.bindings[] | select(.role=="roles/storage.admin")'
If you see the member here, search logs for that resource. If you do not, continue — the access may be inherited or group-based.
- Analyze effective access for the identity.
gcloud asset analyze-iam-policy --identity='serviceAccount:deploy-bot@myproj.iam.gserviceaccount.com' --full-resource-name='//storage.googleapis.com/projects/_/buckets/prod-artifacts' --format=json > /tmp/analyze.json
jq '.analysisResults[] | {attachedResourceFullName, iamBinding: .iamBinding}' /tmp/analyze.json
If attachedResourceFullName points to a project, folder, or organization instead of the bucket, that is your problem: the grant was inherited. Jump to the inherited-policy fix section.
- Query Cloud Audit Logs for
SetIamPolicyat the right scope.
gcloud logging read 'logName:"cloudaudit.googleapis.com%2Factivity" AND protoPayload.methodName="SetIamPolicy" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/storage.admin" AND protoPayload.serviceData.policyDelta.bindingDeltas.member="serviceAccount:deploy-bot@myproj.iam.gserviceaccount.com"' --project=PROJECT_ID --limit=50 --format=json > /tmp/iam-audit.json
jq '.[0] | {timestamp, actor: .protoPayload.authenticationInfo.principalEmail, resource: .protoPayload.resourceName, deltas: .protoPayload.serviceData.policyDelta.bindingDeltas}' /tmp/iam-audit.json
If you get a matching entry with action: "ADD", you have the answer: principalEmail granted it at timestamp. If zero results, do not assume no audit trail yet — continue with broader scope.
- Broaden the search to parent scopes and relevant resources.
gcloud logging read 'logName:"cloudaudit.googleapis.com%2Factivity" AND protoPayload.methodName="SetIamPolicy" AND (protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/storage.admin") AND (protoPayload.serviceData.policyDelta.bindingDeltas.member="serviceAccount:deploy-bot@myproj.iam.gserviceaccount.com")' --folder=FOLDER_ID --limit=50 --format='table(timestamp,protoPayload.authenticationInfo.principalEmail,protoPayload.resourceName)'
Repeat with --organization=ORG_ID if needed. If results appear only at folder/org scope, the effective access was inherited from there. Jump to the wrong-scope or inherited-policy fix section.
- If the actor looks like a service account, inspect impersonation/delegation.
gcloud logging read 'logName:"cloudaudit.googleapis.com%2Factivity" AND protoPayload.methodName="SetIamPolicy" AND protoPayload.authenticationInfo.principalEmail:"gserviceaccount.com"' --project=PROJECT_ID --limit=20 --format=json | jq '.[].protoPayload.authenticationInfo'
If serviceAccountDelegationInfo is present, it can show the originating user or workload chain. That is your answer, not just the final service account identity. Jump to the automation/impersonation fix section.
- If there are still no results, check retention on the searched log bucket.
gcloud logging buckets describe _Default --location=global --project=PROJECT_ID --format='yaml(retentionDays,locked)'
If the event predates retentionDays and you have no sink to another bucket, BigQuery, or archive, the audit event is gone from that location. Jump to the retention fix section.
Fixes
Wrong resource scope
Search the scope where the binding was actually changed, not where you first noticed the effect.
# Project scope
gcloud logging read 'logName:"cloudaudit.googleapis.com%2Factivity" AND protoPayload.methodName="SetIamPolicy" AND protoPayload.serviceData.policyDelta.bindingDeltas.member="user:bob@example.com"' --project=PROJECT_ID --limit=100 --format='table(timestamp,protoPayload.authenticationInfo.principalEmail,protoPayload.resourceName)'
# Folder scope
gcloud logging read 'logName:"cloudaudit.googleapis.com%2Factivity" AND protoPayload.methodName="SetIamPolicy" AND protoPayload.serviceData.policyDelta.bindingDeltas.member="user:bob@example.com"' --folder=FOLDER_ID --limit=100 --format='table(timestamp,protoPayload.authenticationInfo.principalEmail,protoPayload.resourceName)'
# Organization scope
gcloud logging read 'logName:"cloudaudit.googleapis.com%2Factivity" AND protoPayload.methodName="SetIamPolicy" AND protoPayload.serviceData.policyDelta.bindingDeltas.member="user:bob@example.com"' --organization=ORG_ID --limit=100 --format='table(timestamp,protoPayload.authenticationInfo.principalEmail,protoPayload.resourceName)'
Verify it worked:
jq '.[0].protoPayload.authenticationInfo.principalEmail, .[0].timestamp' /tmp/iam-audit.json
Inherited policy from parent resource
Once you confirm inheritance, inspect and, if needed, remediate at the parent where the binding lives.
⚠️ Removing a parent IAM binding can break multiple child resources at once. Review all affected principals and resources before changing it.
# Example: inspect folder policy
gcloud resource-manager folders get-iam-policy FOLDER_ID --format=json | jq '.bindings[] | select(.role=="roles/storage.admin")'
# Example: remove a member from a project-level binding
gcloud projects remove-iam-policy-binding PROJECT_ID --member='serviceAccount:deploy-bot@myproj.iam.gserviceaccount.com' --role='roles/storage.admin'
Verify it worked:
gcloud asset analyze-iam-policy --identity='serviceAccount:deploy-bot@myproj.iam.gserviceaccount.com' --full-resource-name='//storage.googleapis.com/projects/_/buckets/prod-artifacts' --format='value(analysisResults)'
Wrong log query / wrong fields
Use policyDelta.bindingDeltas for the actual add/remove operation and authenticationInfo.principalEmail for the actor.
gcloud logging read 'logName:"cloudaudit.googleapis.com%2Factivity" AND protoPayload.methodName="SetIamPolicy" AND protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD"' --project=PROJECT_ID --limit=20 --format=json | jq '.[] | {timestamp, actor: .protoPayload.authenticationInfo.principalEmail, resource: .protoPayload.resourceName, deltas: .protoPayload.serviceData.policyDelta.bindingDeltas}'
Verify it worked:
gcloud logging read 'protoPayload.methodName="SetIamPolicy"' --project=PROJECT_ID --limit=1 --format='value(protoPayload.authenticationInfo.principalEmail)'
Event aged out of retention
If the event is older than retention, recover from a sink destination if one exists; otherwise document the gap and extend retention going forward.
# Check current retention
gcloud logging buckets describe _Default --location=global --project=PROJECT_ID
# Create a dedicated log bucket with longer retention
gcloud logging buckets create iam-audit-longret --location=global --project=PROJECT_ID --retention-days=365
# Route admin activity logs to it
gcloud logging sinks create iam-audit-sink logging.googleapis.com/projects/PROJECT_ID/locations/global/buckets/iam-audit-longret --project=PROJECT_ID --log-filter='logName:"cloudaudit.googleapis.com%2Factivity" AND protoPayload.methodName="SetIamPolicy"'
Verify it worked:
gcloud logging sinks describe iam-audit-sink --project=PROJECT_ID --format='yaml(filter,destination)'
Automation or impersonation hid the human actor
When a service account appears as the actor, inspect delegation info and the CI/CD system that used it.
gcloud logging read 'logName:"cloudaudit.googleapis.com%2Factivity" AND protoPayload.methodName="SetIamPolicy" AND protoPayload.authenticationInfo.serviceAccountDelegationInfo:*' --project=PROJECT_ID --limit=20 --format=json | jq '.[] | {timestamp, principal: .protoPayload.authenticationInfo.principalEmail, delegation: .protoPayload.authenticationInfo.serviceAccountDelegationInfo}'
If your pipeline applies IAM through Terraform, correlate the timestamp with the job run and VCS commit that changed IAM resources. Verify it worked:
gcloud logging read 'protoPayload.authenticationInfo.serviceAccountDelegationInfo:*' --project=PROJECT_ID --limit=1 --format='value(protoPayload.authenticationInfo.serviceAccountDelegationInfo)'
Group membership change, not direct binding
If the role is granted to a group and the user got access by joining that group, the SetIamPolicy event only tells you who bound the group to the role. You also need the identity provider or directory audit trail for who added the user to the group.
gcloud projects get-iam-policy PROJECT_ID --flatten='bindings[].members' --format='table(bindings.role,bindings.members)' | grep 'group:'
Then query your directory audit logs for membership changes on that group at or before the access start time. Verify it worked:
gcloud projects get-iam-policy PROJECT_ID --flatten='bindings[].members' --format='table(bindings.role,bindings.members)' | grep 'group:platform-admins@example.com'
Prevention
- Route IAM admin activity to a long-retention bucket instead of relying on default retention.
gcloud logging buckets create iam-admin-365d --location=global --project=PROJECT_ID --retention-days=365
gcloud logging sinks create route-iam-admin logging.googleapis.com/projects/PROJECT_ID/locations/global/buckets/iam-admin-365d --project=PROJECT_ID --log-filter='logName:"cloudaudit.googleapis.com%2Factivity" AND protoPayload.methodName="SetIamPolicy"'
- Export IAM change logs to BigQuery for fast historical joins by actor, member, and role.
gcloud logging sinks create iam-admin-bq bigquery.googleapis.com/projects/PROJECT_ID/datasets/audit_logs --project=PROJECT_ID --log-filter='logName:"cloudaudit.googleapis.com%2Factivity" AND protoPayload.methodName="SetIamPolicy"'
- Alert on high-risk role grants using a logs-based metric on
bindingDeltas.action="ADD".
Filter:
logName:"cloudaudit.googleapis.com%2Factivity"
protoPayload.methodName="SetIamPolicy"
protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD"
(protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner" OR protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/iam.securityAdmin" OR protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/resourcemanager.projectIamAdmin")
- In CI, require IAM changes to be isolated in dedicated Terraform plans so the actor and intent are easier to correlate.
terraform plan -target=google_project_iam_binding.prod_admins -out=tfplan-iam
terraform show -json tfplan-iam | jq '.resource_changes[] | select(.type|test("iam")) | .address'
- Snapshot effective IAM policies nightly for critical projects and buckets so you can diff when logs are missing.
gcloud projects get-iam-policy PROJECT_ID --format=json > gs://org-audit-snapshots/iam/PROJECT_ID-$(date +%F).json
gsutil iam get gs://prod-artifacts > /tmp/prod-artifacts-iam-$(date +%F).json
- Prefer group-based grants with directory audit retention, but document that answering "who got the role" may require both Cloud Audit Logs and directory membership logs.
Runbook note: resource binding actor comes from Cloud Audit Logs; group membership actor comes from your IdP/directory audit logs.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI