This page covers Google Cloud Platform Identity & Access Management hardening across the surfaces that determine whether an attacker who lands a credential can pivot to organisation-wide compromise or staged data exfiltration. GCP's IAM model differs from AWS's root-and-Organizations model in three material ways that shape the eight controls below: organisation policy constraints are the primary "deny" primitive (there is no equivalent to a Service Control Policy attached to identities, only constraints attached to resource hierarchy nodes); long-lived service-account keys are the dominant credential-compromise vector, which is why Workload Identity Federation replacing those keys is treated here as a load-bearing preventive control; and VPC Service Controls form a GCP-unique data-exfiltration perimeter around regulated services such as BigQuery and Cloud Storage. Scope is commercial GCP regions; the GCP Sovereign Cloud offerings inherit the same controls but require region-specific endpoints and have separate org-policy enforcement domains.
The mental model: GCP IAM is the product of allow policies (role bindings attached to organisations, folders, projects, or individual resources), org policy constraints (boolean or list-based limits attached to the resource hierarchy that override any allow policy below them), Workload Identity Federation (the recommended replacement for service-account keys when calling GCP from outside GCP, including CI/CD), and VPC Service Controls (an L7 perimeter that restricts which networks can read or write regulated data services). The cross-cutting principles — least privilege, separation of duties, credential rotation, secrets management, MFA — are explained in the General IAM page; this page maps them to GCP primitives. Severity assignments follow the rubric documented in methodology, in particular the worked example EX-MFA-01 which derives a CRITICAL PREVENTIVE for the canonical phishing-resistant MFA case applied directly to control 03 below. The MFA and secrets-management discussions on General IAM — MFA and General IAM — secrets management are the conceptual backbone of controls 02, 03, and 04.
Order matters in this list. Controls 01–03 are CRITICAL PREVENTIVE and address the highest-leverage residual risks in audited GCP organisations: an over-broad Organization Administrator role binding, the continued use of downloadable service-account keys, and incomplete enforcement of mandatory 2-Step Verification (Google's December-2024 rollout completing through 2025; re-verify status at the time of writing). Controls 04–07 are HIGH PREVENTIVE and progressively eliminate service-account keys via Workload Identity Federation, harden default service accounts whose automatic IAM grants are a well-known privilege-escalation surface, forbid Editor/Owner on service accounts, and lock the IAM membership domain to your Workspace / Cloud Identity customer ID so that an attacker who steals a privileged credential cannot grant access to an external account. Control 08 is HIGH PREVENTIVE and is GCP-unique: VPC Service Controls extend the IAM boundary into the network plane and stop data exfiltration even when an attacker holds otherwise-authorised IAM credentials. Reviewing the compliance-frameworks page first will clarify why each control row lists CIS, NIST 800-53 rev5, and ISO 27001/27017 cells in the same order across all four provider pages.
The roles/resourcemanager.organizationAdmin role grants the ability to set IAM policy at the organisation node — the root of the GCP resource hierarchy — and must be bound only to a small, named break-glass group, never to individual day-to-day administrators and never to service accounts. Day-to-day administration should be performed through scoped roles at the folder or project level. Google's published IAM best practices treat this separation as foundational because Organization Administrator can grant itself any other role anywhere in the organisation, including the ability to disable audit logging at the org level (Google Cloud IAM best practices (accessed 2026-05)).
Remediation — gcloud CLI
# Audit: list every member currently bound to roles/resourcemanager.organizationAdmin
# at the organisation node.
ORG_ID=<your-org-id>
gcloud organizations get-iam-policy "$ORG_ID" \
--flatten='bindings[].members' \
--filter='bindings.role=roles/resourcemanager.organizationAdmin' \
--format='value(bindings.members)'
# Replace direct user bindings with a single break-glass Google group.
gcloud organizations remove-iam-policy-binding "$ORG_ID" \
--member='user:departing.admin@example.com' \
--role='roles/resourcemanager.organizationAdmin'
gcloud organizations add-iam-policy-binding "$ORG_ID" \
--member='group:gcp-breakglass-org-admins@example.com' \
--role='roles/resourcemanager.organizationAdmin'
Remediation — Terraform
# Terraform Google provider ~> 5.0
# Organization Administrator is bound to a single named break-glass group.
# Any drift (a user added directly to this role) is removed on the next apply.
resource "google_organization_iam_binding" "org_admin_breakglass_only" {
org_id = var.organization_id
role = "roles/resourcemanager.organizationAdmin"
members = [
"group:gcp-breakglass-org-admins@${var.workspace_domain}",
]
}
import * as gcp from "@pulumi/gcp";
// Bind Organization Administrator only to the break-glass group; drift is
// removed on next pulumi up. Mirrors the Terraform google_organization_iam_binding.
const orgAdminBreakglass = new gcp.organizations.IAMBinding("org-admin-breakglass-only", {
orgId: orgId,
role: "roles/resourcemanager.organizationAdmin",
members: [
"group:gcp-breakglass-org-admins@example.com",
],
});
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
1.5
1.1.1
best-practices
1.1
AC-5; AC-6(7)
A.5.15; A.5.18
CLD.6.3.1
Log signals
Cloud Audit Logs cloudaudit.googleapis.com/activity entries with protoPayload.methodName = "SetIamPolicy" at the organisation, folder, or project resource scope.
protoPayload deltas binding roles/resourcemanager.organizationAdmin, roles/owner, or roles/iam.securityAdmin to a fresh principal outside the documented break-glass group.
Cloud Asset Inventory iamPolicy feed deltas tracking the standing-Organization-Admin membership over time.
Query
logName="organizations/ORG_ID/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.methodName="SetIamPolicy"
AND (protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/resourcemanager.organizationAdmin"
OR protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner"
OR protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/iam.securityAdmin")
AND severity>=NOTICE
Run as a Cloud Logging filter expression against the aggregated organisation log sink. Pin a Log Router sink to BigQuery for retention beyond the default 400-day Activity window if a longer audit horizon is needed.
Alert threshold
Any SetIamPolicy binding the organisation-admin / owner / security-admin roles with severity >= NOTICE outside documented change windows — page on first occurrence.
Starting point: tune per environment using Cloud Logging resource.type="organization" baselines over a 30-day calibration window.
Initial response
Verify the binding against the documented change-management record and the principal who initiated the call (protoPayload.authenticationInfo.principalEmail); if no ticket exists, treat as confirmed compromise.
Roll back via the Cloud Asset Inventory IAM Recommender: revoke the standing binding, surface least-privilege replacement role recommendations, and rotate any service-account keys the principal issued in the prior 24h.
Escalate per general/ir.html — open an incident, export the relevant Cloud Audit Logs window to BigQuery for forensic analysis, and re-confirm Org Policy constraints (iam.allowedPolicyMemberDomains, iam.disableServiceAccountKeyCreation) still apply.
Long-lived, downloadable service-account keys are the single highest-frequency credential-compromise vector in cloud breach post-mortems — JSON key files end up in public Git repositories, container layers, developer laptops, and CI environment variables, where they are harvested by automated scanners within minutes of exposure. Disable creation of new keys organisation-wide via the boolean constraint iam.disableServiceAccountKeyCreation attached at the organisation node, and rely on Workload Identity Federation (control 04) or attached service accounts for runtime credentials (Google Cloud organization policy constraint reference (accessed 2026-05)). CIS GCP Foundation v4.0.0 1.4 codifies this restriction; re-verify the sub-ID against the v4.0.0 PDF at the time of writing.
Remediation — gcloud CLI
# Apply the boolean constraint at the organisation node so it inherits
# across every folder and project.
ORG_ID=<your-org-id>
cat > disable-sa-key-creation.yaml <<'EOF'
name: organizations/ORG_ID_PLACEHOLDER/policies/iam.disableServiceAccountKeyCreation
spec:
rules:
- enforce: true
EOF
sed -i "s/ORG_ID_PLACEHOLDER/${ORG_ID}/" disable-sa-key-creation.yaml
gcloud org-policies set-policy disable-sa-key-creation.yaml
# Audit: list existing user-managed keys across all service accounts in a project.
PROJECT=<your-project-id>
gcloud iam service-accounts list --project="$PROJECT" \
--format='value(email)' \
| while read sa; do
gcloud iam service-accounts keys list \
--iam-account="$sa" \
--managed-by=user \
--format='value(name,validAfterTime)'
done
import * as gcp from "@pulumi/gcp";
// Boolean constraint applied at the organisation node; inherits across folders/projects.
const disableSaKeyCreation = new gcp.orgpolicy.Policy("disable-sa-key-creation", {
name: `organizations/${orgId}/policies/iam.disableServiceAccountKeyCreation`,
parent: `organizations/${orgId}`,
spec: {
rules: [{ enforce: "TRUE" }],
},
});
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
1.4
1.1.3
1.4
1.14
IA-5(1); IA-5(7)
A.5.17; A.8.2
CLD.6.3.1
Log signals
Cloud Audit Logs entries on cloudaudit.googleapis.com/activity with protoPayload.methodName="google.iam.admin.v1.CreateServiceAccountKey" — any successful call indicates a fresh user-managed JSON key has been issued.
Org Policy drift events where iam.disableServiceAccountKeyCreation changes from enforce: true to enforce: false (orgpolicy.googleapis.com Policy.update).
Cloud Asset Inventory snapshot diffs showing the count of iam.googleapis.com/ServiceAccountKey resources rising over a stable baseline — co-relates with key-leak pivot.
Query
logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND (protoPayload.methodName="google.iam.admin.v1.CreateServiceAccountKey"
OR protoPayload.methodName="google.cloud.orgpolicy.v2.OrgPolicy.UpdatePolicy")
AND resource.type=("service_account" OR "organization")
Aggregate the result via a Cloud Logging log-based metric and ship it through Cloud Monitoring; pin a Log Router sink to BigQuery if more than 400 days of key-issuance history is required for forensic reconstruction.
Alert threshold
Page on any CreateServiceAccountKey outside an explicitly allow-listed automation principal — the steady-state rate after enforcement should be effectively zero.
Page immediately on any Org Policy update that turns iam.disableServiceAccountKeyCreation off; no calibration window — the constraint is a hard organisation invariant.
Initial response
Capture protoPayload.authenticationInfo.principalEmail, requestMetadata.callerIp, and the target service-account email; treat the new key as compromised-by-default until the issuing principal confirms ticket reference and intended consumer.
Disable the freshly created key with gcloud iam service-accounts keys disable, then schedule deletion after 24 hours of telemetry validation; re-assert the Org Policy constraint at the organisation node.
Pivot to Workload Identity Federation per control 04: cut the hard-coded JSON consumer over to a federated identity pool and bind the OIDC subject claim instead of issuing another long-lived credential.
Enforce mandatory 2-Step Verification (2SV) for every human identity that can sign in to the Google Cloud console, with phishing-resistant FIDO2 security keys as the preferred second factor and TOTP authenticator apps as the minimum-acceptable fallback. Google announced in 2024 that 2SV is becoming mandatory across all Google Cloud accounts with phased rollouts completing through 2025; treat enforcement as in-flight policy and re-verify the rollout status at the time of writing (Google Cloud — mandatory MFA rollout announcement 2024 (accessed 2026-05)). CIS GCP Foundation v4.0.0 1.2 codifies the requirement; verify the sub-ID against the v4.0.0 PDF.
Remediation — gcloud CLI
# 2SV enforcement is configured in the Cloud Identity / Workspace Admin Console:
# Security > Authentication > 2-Step Verification
# - Enforcement: ON
# - Methods: Security key (preferred) or Any
# - New user enrollment period: as short as operationally feasible
#
# Audit current 2SV status across the organisation via Cloud Identity groups
# and Workspace user inventory:
gcloud identity groups memberships list \
--group-email='all-admins@example.com' \
--format='value(preferredMemberKey.id)'
# Per-user 2SV status is queried via the Admin SDK Directory API
# (gcloud does not expose it directly); the response field is `isEnrolledIn2Sv`.
TOKEN=$(gcloud auth print-access-token)
curl -s -H "Authorization: Bearer $TOKEN" \
'https://admin.googleapis.com/admin/directory/v1/users/admin@example.com?projection=full' \
| jq '{primaryEmail, isEnrolledIn2Sv, isEnforcedIn2Sv}'
Remediation — Terraform
# Terraform Google provider ~> 5.0
# 2SV enforcement itself is configured in the Workspace Admin Console;
# Terraform manages Cloud Identity group membership so that audit lists
# of admins required to enrol in 2SV stay in sync with the directory.
resource "google_cloud_identity_group" "admins" {
display_name = "GCP Administrators"
parent = "customers/${var.workspace_customer_id}"
group_key {
id = "gcp-admins@${var.workspace_domain}"
}
labels = {
"cloudidentity.googleapis.com/groups.discussion_forum" = ""
}
}
resource "google_cloud_identity_group_membership" "admin_user" {
group = google_cloud_identity_group.admins.id
preferred_member_key {
id = "alice@${var.workspace_domain}"
}
roles { name = "MEMBER" }
roles { name = "OWNER" }
}
Remediation — Infrastructure Manager
Remediation — Pulumi (TypeScript)
import * as gcp from "@pulumi/gcp";
import * as cloudidentity from "@pulumi/gcp/cloudidentity";
// Cloud Identity group + membership (the enrolment workflow is admin-console driven;
// Pulumi declares the group + admin members; MFA enforcement itself is a Workspace policy).
const admins = new cloudidentity.Group("admins", {
displayName: "GCP Admins (MFA mandatory)",
parent: `customers/${customerId}`,
groupKey: { id: "gcp-admins@example.com" },
labels: { "cloudidentity.googleapis.com/groups.security": "" },
});
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
1.10
1.1.2
1.2
1.7
IA-2(1); IA-2(2)
A.5.17; A.8.5
n/a
Log signals
Workspace Admin SDK login audit reports with type="2sv_disable" or type="2sv_enroll" events streamed into Cloud Logging via the Workspace audit-log connector.
Cloud Audit Logs protoPayload.serviceName="admin.googleapis.com" events where the Workspace 2SV enforcement setting is toggled at the OU level.
Cloud Identity user inventory pulls (Directory API users.list?projection=full) where the proportion of admin-group members with isEnforcedIn2Sv=false rises above zero.
Query
logName=~"organizations/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.serviceName="admin.googleapis.com"
AND (protoPayload.metadata.event.name="2sv_disable"
OR protoPayload.metadata.event.name="ALLOW_STRONG_AUTHENTICATION"
OR protoPayload.methodName=~".*UpdateOrgUnit.*")
AND resource.type="audited_resource"
Stream the Workspace admin audit feed into Cloud Logging using the Workspace audit-log export and query it side-by-side with GCP-side Cloud Audit Logs entries to correlate identity-plane changes with downstream IAM policy mutations.
Alert threshold
Page on any 2sv_disable event at the admin OU; a single occurrence is a candidate compromise of the Workspace super-admin role.
Daily cron: alert if any account in gcp-admins@ shows isEnrolledIn2Sv=false for more than the enrolment grace window declared in policy.
Initial response
Suspend the affected user in Workspace, force a sign-out across all sessions, and revoke active OAuth grants via users.tokens.delete in the Directory API.
Re-enable 2SV enforcement on the OU, require security-key enrollment for any account that held privileged IAM bindings during the 2SV gap window, and audit every SetIamPolicy call attributable to the user in that gap.
Escalate to the Workspace super-admin break-glass owner per general/ir.html; if the disable was performed by a super-admin principal, treat the Workspace customer-id boundary as compromised and begin tenant-level containment.
Workload Identity Federation (WIF) is GCP's recommended replacement for downloadable service-account keys when authenticating from external workloads — GitHub Actions, GitLab CI, on-premises Kubernetes, AWS, Azure, or any OIDC- or SAML-emitting identity provider. WIF exchanges an external-identity-provider token for a short-lived GCP access token via the Security Token Service, eliminating the need to store a long-lived JSON key in CI secrets. Combined with control 02 (no-SA-keys), WIF collapses the credential-rotation problem to "re-issue an OIDC token from the IdP", which the IdP already does automatically (Google Cloud Workload Identity Federation documentation (accessed 2026-05)).
Remediation — gcloud CLI
# Create a Workload Identity pool and an OIDC provider for GitHub Actions.
PROJECT=<your-project-id>
POOL_ID=github-pool
PROVIDER_ID=github-provider
gcloud iam workload-identity-pools create "$POOL_ID" \
--project="$PROJECT" \
--location=global \
--display-name="GitHub Actions pool"
gcloud iam workload-identity-pools providers create-oidc "$PROVIDER_ID" \
--project="$PROJECT" \
--location=global \
--workload-identity-pool="$POOL_ID" \
--display-name="GitHub OIDC" \
--attribute-mapping='google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref' \
--attribute-condition='assertion.repository_owner == "your-org"' \
--issuer-uri='https://token.actions.githubusercontent.com'
# Allow a specific repository to impersonate the deploy service account.
gcloud iam service-accounts add-iam-policy-binding \
"deploy@${PROJECT}.iam.gserviceaccount.com" \
--role='roles/iam.workloadIdentityUser' \
--member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/attribute.repository/your-org/your-repo"
Cloud Audit Logs on iam.googleapis.com for WorkloadIdentityPool and WorkloadIdentityPoolProvider mutations — particularly CreateWorkloadIdentityPoolProvider with an oidc.allowedAudiences list that includes a wildcard or an unfamiliar issuer URI.
Service-account impersonation events: protoPayload.methodName="GenerateAccessToken" with protoPayload.authenticationInfo.principalSubject bearing a principal://iam.googleapis.com/projects/-/locations/global/workloadIdentityPools/.../subject/... identity, especially where the subject claim drifts from the documented federation map.
Org Policy state for iam.workloadIdentityPoolProviders allow-list — drift to a previously unseen issuer host should fire.
Query
logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND resource.type="iam_workload_identity_pool_provider"
AND (protoPayload.methodName="google.iam.v1.WorkloadIdentityPools.CreateWorkloadIdentityPoolProvider"
OR protoPayload.methodName="google.iam.v1.WorkloadIdentityPools.UpdateWorkloadIdentityPoolProvider")
Pair this Cloud Logging filter with a second filter on iamcredentials.googleapis.com/GenerateAccessToken events so that pool-provider drift can be joined to the federated tokens it subsequently issued — the join key is the provider resource name in the impersonation principal subject.
Alert threshold
Page on every CreateWorkloadIdentityPoolProvider call — pool providers are rare, security-critical, configuration-as-code artefacts and a console-driven creation is a candidate red-team or social-engineering path.
Page on any UpdateWorkloadIdentityPoolProvider that loosens the attribute condition (CEL expression) or adds a wildcard allowed_audiences entry.
Initial response
Capture the full protoPayload.request body and the diff against the prior provider state via Cloud Asset Inventory history; identify which OIDC issuer was added and which subject-attribute mapping was altered.
Disable the pool provider (gcloud iam workload-identity-pools providers update-oidc --disabled) until the change is approved; revoke any access tokens issued through it during the window via iamcredentials.tokens.revoke.
Audit downstream GenerateAccessToken events keyed on the provider resource name and correlate to data-plane API calls — every action taken under those tokens needs replay validation.
The Compute Engine and App Engine default service accounts are created automatically in every new project and, historically, were automatically granted roles/editor at the project level — a long-standing privilege-escalation surface because any compute workload running with the attached default SA inherits broad project-wide write access. Enforce the boolean org-policy constraint iam.automaticIamGrantsForDefaultServiceAccounts at the organisation node to suppress these automatic grants for newly-created projects, then explicitly bind narrow custom roles to default SAs where they are still in use (Google Cloud organization policy constraint reference (accessed 2026-05)). CIS GCP Foundation v4.0.0 1.5 codifies this; re-verify the sub-ID against the v4.0.0 PDF. Where users genuinely need just-in-time elevated access for break-glass workflows, pair this control with Privileged Access Manager (PAM) — GA per Google's 2024 announcement; re-verify GA status at writing time — which issues time-bound role grants with approval flow and full audit trail rather than relying on standing default-SA permissions.
Remediation — gcloud CLI
# Enforce the boolean constraint at the organisation node.
ORG_ID=<your-org-id>
cat > no-default-sa-grants.yaml <<EOF
name: organizations/${ORG_ID}/policies/iam.automaticIamGrantsForDefaultServiceAccounts
spec:
rules:
- enforce: true
EOF
gcloud org-policies set-policy no-default-sa-grants.yaml
# Audit: identify existing default SAs still holding roles/editor.
PROJECT=<your-project-id>
PROJECT_NUMBER=$(gcloud projects describe "$PROJECT" --format='value(projectNumber)')
gcloud projects get-iam-policy "$PROJECT" \
--flatten='bindings[].members' \
--filter="bindings.role=roles/editor AND bindings.members:${PROJECT_NUMBER}-compute@" \
--format='value(bindings.members,bindings.role)'
Cloud Audit Logs SetIamPolicy entries at project scope where protoPayload.serviceData.policyDelta.bindingDeltas adds a binding whose member matches serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com or serviceAccount:PROJECT_NUMBER@appspot.gserviceaccount.com with roles/editor.
Org Policy constraint iam.automaticIamGrantsForDefaultServiceAccounts mutation events; the constraint should remain enforce: true at the organisation node.
Compute Engine API calls instances.insert attaching the default compute service account without an explicit --service-account override — surfaced via Cloud Logging filter on request.serviceAccounts.email.
Query
logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.methodName="SetIamPolicy"
AND resource.type="project"
AND protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD"
AND protoPayload.serviceData.policyDelta.bindingDeltas.member=~"serviceAccount:[0-9]+-compute@developer.gserviceaccount.com"
AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/editor"
Run as a saved Cloud Logging filter pinned to the aggregated organisation sink; correlate hits with Cloud Asset Inventory IAM Recommender output, which produces a least-privilege role suggestion for each binding observed.
Alert threshold
Page on any binding that grants roles/editor (or any role with *.setIamPolicy permissions) to the default compute or App Engine service account in any project — the policy is project-wide and the steady-state count should be zero.
Page on any flip of iam.automaticIamGrantsForDefaultServiceAccounts away from enforced.
Initial response
Identify VMs and Cloud Functions currently running under the default service account via Cloud Asset Inventory compute.googleapis.com/Instance queries; enumerate the API surface those workloads call before stripping the role.
Replace the binding with a purpose-built service account holding only the predicate-narrow roles the workload requires; redeploy with --service-account= explicitly set on the next deployment slot.
Re-assert the Org Policy constraint and run the IAM Recommender to confirm no transitive Editor-equivalent role survives via custom roles.
Service accounts must never hold roles/owner or roles/editor at project, folder, or organisation level. These primitive roles bundle thousands of permissions, including the ability to create and impersonate other service accounts, and a workload that needs that much power is almost always a workload whose architecture has not yet been decomposed into purpose-built custom roles. Use IAM Recommender to identify over-privileged SAs from observed call patterns, then replace primitive role bindings with narrow predefined or custom roles. Pair this with Privileged Access Manager (GA per Google's 2024 announcement; re-verify GA status at writing time) where short-lived elevated grants are unavoidable so that no SA carries standing Editor/Owner authority (Google Cloud IAM best practices (accessed 2026-05)). CIS GCP Foundation v4.0.0 1.6 codifies the prohibition; re-verify the sub-ID against the v4.0.0 PDF.
Remediation — gcloud CLI
# Audit: list every service account bound to roles/owner or roles/editor.
PROJECT=<your-project-id>
for role in roles/owner roles/editor; do
gcloud projects get-iam-policy "$PROJECT" \
--flatten='bindings[].members' \
--filter="bindings.role=${role} AND bindings.members:serviceAccount:" \
--format="value(bindings.members)"
done
# Remediation: replace the primitive binding with a scoped custom role.
gcloud projects remove-iam-policy-binding "$PROJECT" \
--member='serviceAccount:ci-pipeline@example.iam.gserviceaccount.com' \
--role='roles/owner'
gcloud projects add-iam-policy-binding "$PROJECT" \
--member='serviceAccount:ci-pipeline@example.iam.gserviceaccount.com' \
--role='projects/example/roles/ciPipelineDeployer'
# Pull IAM Recommender suggestions for over-privileged bindings.
gcloud recommender recommendations list \
--project="$PROJECT" \
--location=global \
--recommender=google.iam.policy.Recommender \
--format='value(name,recommenderSubtype,content.overview)'
Remediation — Terraform
# Terraform Google provider ~> 5.0
# Bind the CI service account to a narrow custom role, never roles/owner or roles/editor.
resource "google_project_iam_custom_role" "ci_pipeline_deployer" {
project = var.project_id
role_id = "ciPipelineDeployer"
title = "CI Pipeline Deployer"
description = "Minimum permissions for the deploy pipeline."
permissions = [
"run.services.update",
"run.services.get",
"artifactregistry.repositories.uploadArtifacts",
"storage.objects.create",
"storage.objects.get",
]
}
resource "google_project_iam_member" "ci_pipeline_binding" {
project = var.project_id
role = google_project_iam_custom_role.ci_pipeline_deployer.id
member = "serviceAccount:ci-pipeline@${var.project_id}.iam.gserviceaccount.com"
}
Remediation — Config Connector
apiVersion: iam.cnrm.cloud.google.com/v1beta1
kind: IAMCustomRole
metadata:
name: ci-pipeline-deployer
namespace: config-control
spec:
title: "CI Pipeline Deployer (scoped)"
description: "Minimum permissions for the CI deploy pipeline"
stage: GA
permissions:
- run.services.create
- run.services.update
- artifactregistry.repositories.downloadArtifacts
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
best-practices
1.1.4
1.6
best-practices
AC-6; AC-6(1)
A.5.15; A.5.16
CLD.6.3.1
Log signals
Cloud Audit Logs SetIamPolicy deltas where any serviceAccount: principal receives roles/owner, roles/editor, roles/iam.securityAdmin, or any custom role aggregating iam.roles.update and resourcemanager.projects.setIamPolicy.
Service-account impersonation chains: iamcredentials.googleapis.com/GenerateAccessToken events where the impersonating principal is a service account itself, indicating a non-human pivot toward broader scope.
Privileged Access Manager (PAM) audit events showing standing grants that should instead be just-in-time entitlements.
Query
logName=~"projects/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.methodName="SetIamPolicy"
AND protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD"
AND protoPayload.serviceData.policyDelta.bindingDeltas.member=~"serviceAccount:.*"
AND (protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner"
OR protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/editor"
OR protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/iam.securityAdmin")
Schedule a weekly Cloud Asset Inventory IAM-state export to BigQuery and run a saved query that joins policyDelta.bindingDeltas on principal-type to surface the population of service accounts holding privileged primitive roles in a single CSV.
Alert threshold
Page on any new binding that grants a privileged role to a service-account principal; the documented allow-list of automation accounts is short and changes through a change-management ticket.
Surface (do not page) impersonation chains where a non-privileged SA calls GenerateAccessToken for a privileged target SA more than once per hour — review weekly.
Initial response
Trace the chain of impersonation back to the originating human or workload identity via the delegationInfo.principalSubject chain in iamcredentials audit entries.
Revoke the privileged binding, replace it with a PAM entitlement that requires explicit grant per change window, and rotate any tokens the chain produced during the standing-grant window.
Re-run the IAM Recommender on the project to surface alternative narrower roles, and store the recommendation as the documented baseline for the workload owner's next review.
The list-based org-policy constraint iam.allowedPolicyMemberDomains restricts which Workspace / Cloud Identity customer IDs may appear in any IAM policy member field across the organisation. With this constraint enforced and populated with your organisation's customer ID(s), an attacker who compromises a privileged credential cannot add an external gmail.com address or a different Workspace tenant as a project Owner — the org policy denies the IAM binding before it is written. Pair with monitoring of org-policy violation events for early-warning signals (Google Cloud organization policy constraint reference (accessed 2026-05)).
Cloud Audit Logs orgpolicy.googleapis.com entries on Policy.UpdatePolicy targeting iam.allowedPolicyMemberDomains — additions to the allow-list or full removal of the constraint both deserve attention.
Denied SetIamPolicy requests carrying protoPayload.status.code=7 with a message string referencing customer ID — those are blocked attempts to bind a foreign-domain principal and they document attempted external grants.
Cloud Asset Inventory diffs on the constraint resource: a missing denyAll or shifted allowedValues array is the steady-state alarm condition.
Query
logName=~"organizations/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.serviceName="orgpolicy.googleapis.com"
AND protoPayload.methodName=~".*UpdatePolicy"
AND protoPayload.request.policy.name=~".*iam.allowedPolicyMemberDomains"
This Cloud Logging filter runs against the organisation-scoped sink. Pair it with a second saved query that aggregates blocked SetIamPolicy attempts on resource.type="project" so attempted bypasses are visible alongside successful constraint mutations.
Alert threshold
Page on any update that adds a new customer-id to the allow-list; the value list should match the documented set of partner tenants exactly.
Page on outright removal of the constraint or relaxation from denyAll: false with empty values — that pattern silently permits any principal domain.
Initial response
Pull the prior constraint state from Cloud Asset Inventory history and diff against the live policy to confirm the precise additions; identify the principal that issued the UpdatePolicy call.
Roll the constraint back to the prior state via gcloud org-policies set-policy against the captured baseline YAML; revoke any IAM bindings created against the newly admitted customer-id during the window.
Open an incident under general/ir.html and audit all SetIamPolicy activity in the gap window against the new allow-list members — the constraint-edit and the binding-edit are typically minutes apart.
VPC Service Controls (VPC SC) is a GCP-unique data-exfiltration perimeter that extends the IAM boundary into the network plane. A service perimeter wraps a set of regulated GCP services — BigQuery, Cloud Storage, Cloud Spanner, Cloud Pub/Sub, Artifact Registry, Vertex AI, and others — and denies all API access that originates outside the perimeter even when the calling principal holds valid IAM credentials. This stops the canonical attack where an attacker who has stolen a service-account credential exports BigQuery results to a Cloud Storage bucket in an external organisation. There is no direct equivalent on AWS, Azure, or OCI; the closest parallel is AWS SCP region-deny lists combined with Access Analyzer external-share findings, but VPC SC operates at request time and denies the call, where the AWS combination operates at policy-author time and after-the-fact (Google Cloud VPC Service Controls overview (accessed 2026-05)).
Remediation — gcloud CLI
# Create an access policy at the organisation node (one per org).
ORG_ID=<your-org-id>
POLICY_ID=$(gcloud access-context-manager policies create \
--organization="$ORG_ID" \
--title="default-access-policy" \
--format='value(name)')
# Create a service perimeter restricting BigQuery and Cloud Storage to in-perimeter projects.
gcloud access-context-manager perimeters create regulated_data \
--policy="$POLICY_ID" \
--title="Regulated data perimeter" \
--resources='projects/111111,projects/222222' \
--restricted-services='bigquery.googleapis.com,storage.googleapis.com' \
--perimeter-type=regular
Remediation — Terraform
# Terraform Google provider ~> 5.0
resource "google_access_context_manager_access_policy" "default" {
parent = "organizations/${var.organization_id}"
title = "default-access-policy"
}
resource "google_access_context_manager_service_perimeter" "regulated_data" {
parent = "accessPolicies/${google_access_context_manager_access_policy.default.name}"
name = "accessPolicies/${google_access_context_manager_access_policy.default.name}/servicePerimeters/regulated_data"
title = "Regulated data perimeter"
status {
resources = [
"projects/${var.bq_project_number}",
"projects/${var.gcs_project_number}",
]
restricted_services = [
"bigquery.googleapis.com",
"storage.googleapis.com",
"spanner.googleapis.com",
]
}
}
Cloud Audit Logs on accesscontextmanager.googleapis.com with method names matching ServicePerimeters.patch, ServicePerimeters.replaceAll, or AccessLevel.update at the access-policy resource scope.
VPC-SC denied-request entries on protoPayload.metadata.violationReason — a sudden drop in violation counts after a perimeter edit often signals that the perimeter was widened rather than that exfiltration paths were closed.
Dry-run mode transitions: useExplicitDryRunSpec flipping from true to false indicates an experimental relaxation has been promoted to enforce mode.
Query
logName=~"organizations/.*/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.serviceName="accesscontextmanager.googleapis.com"
AND (protoPayload.methodName=~".*ServicePerimeters.*"
OR protoPayload.methodName=~".*AccessLevels.*")
AND resource.type="audited_resource"
Stream this Cloud Logging filter into a Cloud Monitoring log-based metric grouped by protoPayload.request.servicePerimeter.name so that change frequency per perimeter becomes visible in a single dashboard; cross-reference against the perimeter dry-run delta report Google emits weekly.
Alert threshold
Page on any ServicePerimeters.patch that removes a service from restrictedServices or expands resources beyond the documented project allow-list.
Page on any new egressPolicy rule admitting a new identity or destination project; these rules are explicit perforations of the perimeter and should match a change ticket.
Initial response
Pull the patched perimeter spec from Cloud Asset Inventory history; diff against the prior version to identify the exact rule added or service removed; capture the protoPayload.authenticationInfo.principalEmail.
Revert via the captured baseline policy and re-apply with gcloud access-context-manager perimeters update; if a service was removed, restore it; if an egress policy was added, suspend it.
Audit Cloud Logging for cross-perimeter API calls during the relaxation window — particularly storage.googleapis.com object reads with source IP outside the perimeter's allowed access levels — and treat any successful call as candidate exfiltration.