GCP IAM Deny Policies: How They Override Allows
GCP IAM deny policies evaluate after the allow union: attachment points, a key-creation deny you can test, lockout paths, and exceptions that do not become standing admin.
GCP allow policies are additive: every matching binding counts. IAM deny policies sit on top of that union and subtract. If you only read the project IAM page, you will not see them. If you only add more allows, you will not beat them.
This is the deny evaluation page: math, attachment, a concrete key-creation policy, lockout. Broader IAM hygiene (basic roles, default Compute SA, federation) stays in GCP IAM security hardening. Do not treat deny as a way to keep Editor “but without keys”; remove the Editor binding too. Official overview: IAM deny.
Allow union minus deny
Evaluation order that operators actually need:
- Collect every allow binding that applies (org + folder + project + resource) including conditions that pass.
- Collect every deny rule that applies at org, folder, and project on the path.
- If any deny rule matches the principal and permission (and its condition is true), the request is denied—unless the principal is excepted on that rule.
- Otherwise, if any allow remains, the request is allowed.
There is no “most specific allow wins.” A project Owner does not override a folder deny. Conditions on the allow never punch through a deny.
# Deny policies are not listed by get-iam-policy
gcloud projects get-iam-policy "${PROJECT_ID}" --format=json | jq '.bindings | length'
gcloud iam policies list \
--attachment-point="cloudresourcemanager.googleapis.com/folders/${FOLDER_ID}" \
--kind=denypolicies
Failure mode: Policy Intelligence “who can create keys?” still shows a custom role that includes the permission. Analyzer’s allow graph can lag deny, or you queried before the deny etag propagated. Prove with a real API call from a test user, not only the analyzer screenshot.
Permissions in deny rules use the IAM deny permission format service.googleapis.com/resource.verb (for example iam.googleapis.com/serviceAccountKeys.create), not always the same string you put in a custom role. Wrong spelling = deny never matches = false sense of safety.
Attachment points
| Attachment | Applies to | Use |
|---|---|---|
| Organization | Entire org | Permissions no human should have anywhere (resourcemanager.googleapis.com/organizations.setIamPolicy except break-glass) |
| Folder | That folder and descendants | Nonprod experiments; prod folder key-creation deny |
| Project | That project only | Team-specific denials you are not ready to lift to the folder |
gcloud iam policies create deny-sa-keys-prod \
--attachment-point="cloudresourcemanager.googleapis.com/folders/${PROD_FOLDER}" \
--kind=denypolicies \
--policy-file=deny-sa-keys.yaml
You cannot attach a deny policy to //cloudresourcemanager.googleapis.com/projects/P/buckets/B. If the goal is “this bucket is not publicly writable,” that is IAM on the bucket plus org policy storage.publicAccessPrevention, not a deny policy.
Failure mode: policy created on a project that is not the one CI uses (separate project for Cloud Build). Keys still get created in the Build project. List deny policies at org, each folder on the path, and the project.
Limit: a limited number of deny policies per attachment point. Do not mint one policy per permission; group related denies.
Example deny on key creation
deny-sa-keys.yaml:
displayName: Deny service account key creation
rules:
- denyRule:
deniedPrincipals:
- principalSet://goog/public:all
deniedPermissions:
- iam.googleapis.com/serviceAccountKeys.create
- iam.googleapis.com/serviceAccountKeys.upload
exceptionPrincipals:
- principalSet://goog/group/gcp-breakglass@example.com
Test on a nonprod folder first:
# As a normal user in the test project (should fail after deny)
gcloud iam service-accounts keys create /tmp/k.json \
--iam-account="test-sa@${PROJECT}.iam.gserviceaccount.com"
# Expect PERMISSION_DENIED citing deny
Then delete /tmp/k.json if it succeeded (you are not done). Inventory leftovers:
gcloud iam service-accounts keys list --iam-account="${SA}" --format="table(name,validAfterTime)"
Org policy iam.disableServiceAccountKeyCreation still matters: it blocks the same class of mistake at the constraint layer and shows up in a different console. Deny is the IAM-plane backstop when someone has a role that includes key APIs. The hardening guide walks the constraint + federation sequence; this file is the deny JSON and the lockout story.
Failure mode: deniedPrincipals set to a single user while CI uses a service account. The SA is not that user. Use principalSet://goog/public:all plus a tight exception group, or list principal://iam.googleapis.com/projects/.../serviceAccounts/... explicitly if you must.
Lockout risks
Deny *.setIamPolicy / iam.googleapis.com/policies.* without an exception and you cannot patch the deny policy. Deny orgpolicy.googleapis.com/policy.set and you cannot relax a constraint either. The support ticket is real.
Safer rollout:
- Attach on a nonprod folder with exception = your admin group and a second break-glass group.
- Confirm a throwaway user is denied; confirm break-glass can still
gcloud iam policies update. - Only then clone to prod folder.
- Never start with org-level
deniedPermissions: ["*"].
gcloud iam policies get deny-sa-keys-prod \
--attachment-point="cloudresourcemanager.googleapis.com/folders/${PROD_FOLDER}" \
--kind=denypolicies
If you are already locked out, org-level break-glass (stored hardware keys, not in the denied group) must still hold roles/iam.denyAdmin or equivalent outside the denied principal set. Document that identity in the runbook; do not store it in the same IdP group you just denied.
Failure mode: exception group synced from everyone in Engineering. Deny becomes theater. Membership of the exception group is a CIEM control, same as a standing Owner.
When a public Cloud Run SA can still bigquery.tables.getData, deny did not cause that—the allow bindings did. Path ranking is attack path analysis. Deny is for permissions that should not exist even when someone pastes a wide role.
Checklist
- Deny policies listed at org + prod folder + project; not inferred from get-iam-policy
- Permission strings verified against deny-permission format; test call from a non-excepted user
- Key-create/upload denied on prod folder; existing keys inventoried and deleted
- Exception group is break-glass, reviewed, not
allUsersof Engineering - Update permission on the deny policy itself still held by a tested identity
- Nonprod folder used as the canary; org-wide deny last
- Editor/Owner removal still done per GCP IAM hardening—deny is not a substitute
Related: GCP IAM security hardening · CIEM explained