IAM answers “who may act”. Organization Policy answers “what may exist at all” — and it is the control that stops a project owner from doing something the organisation has decided nobody does.
Topic 1: Two Different Systems
IAM is additive: a binding grants a principal a role on a resource. More bindings mean more permission.
Organization Policy is subtractive: a constraint removes options from everyone in scope, including project owners and including the person who set it. There is no role that overrides it — you change the policy or you do not do the thing.
Effective permission = what IAM grants ∩ what org policy still allows
That intersection is why “I have roles/owner and it still says permission denied” is a normal GCP experience, and why the Policy Troubleshooter (covered in the failure playbook) exists to tell you which of the two denied you.
Constraints come in two shapes:
- Boolean — on or off.
constraints/compute.requireOsLogin,constraints/sql.restrictPublicIp. - List — allow or deny specific values.
constraints/gcp.resourceLocations,constraints/compute.vmExternalIpAccess.
Topic 2: The Constraints Worth Setting on Day One
# No external IPs on VMs — the single largest reduction in attack surface
gcloud resource-manager org-policies deny compute.vmExternalIpAccess \
--organization=123456789012 --all
# Data residency, enforced rather than documented
gcloud resource-manager org-policies allow gcp.resourceLocations \
in:eu-locations --folder=987654321
# No downloadable service-account keys — see the IAM lesson for why
gcloud resource-manager org-policies enable-enforce \
iam.disableServiceAccountKeyCreation --organization=123456789012
# Nobody outside your Cloud Identity domain can be granted a role
gcloud resource-manager org-policies allow iam.allowedPolicyMemberDomains \
C0xxxxxxx --organization=123456789012
The set most organisations converge on, and what each prevents:
| Constraint | Prevents |
|---|---|
compute.vmExternalIpAccess | A VM appearing directly on the internet |
iam.disableServiceAccountKeyCreation | Long-lived JSON keys leaving the platform |
iam.allowedPolicyMemberDomains | Granting access to a personal Gmail account |
storage.publicAccessPrevention | A publicly readable bucket, ever |
sql.restrictPublicIp | A Cloud SQL instance with a public address |
compute.requireShieldedVm | Booting an image without integrity monitoring |
gcp.resourceLocations | Data created outside your permitted regions |
compute.disableSerialPortAccess | An unaudited console path into a VM |
compute.restrictVpcPeering | A peering connection to an unknown network |
iam.allowedPolicyMemberDomains is the one that surprises people later: it also blocks granting roles to allUsers and allAuthenticatedUsers, which is usually the intent — and it blocks a legitimate external contractor, which is why the exception path matters.
Topic 3: Inheritance, Merging and Exceptions
A policy set at the organisation applies to every folder and project beneath it. A child can:
- Inherit (the default).
- Merge with parent for list constraints — add values, within what the parent allows.
- Override — only if the parent has not set
enforcewith inheritance locked.
# A documented exception, applied to ONE project
name: projects/legacy-vendor-integration/policies/compute.vmExternalIpAccess
spec:
inheritFromParent: false
rules:
- values:
allowedValues:
- projects/legacy-vendor-integration/zones/europe-west1-b/instances/vendor-gw
gcloud resource-manager org-policies set-policy exception.yaml \
--project=legacy-vendor-integration
Exceptions should be narrow, project-scoped and documented in the same commit as the reason. An exception at the folder level is not an exception; it is the policy for that folder.
Read the effective policy rather than the one you set — inheritance means the answer is rarely in one place:
gcloud resource-manager org-policies describe compute.vmExternalIpAccess \
--project=checkout-prod --effective
Topic 4: Dry Run First, Always
An enforced constraint blocks real work immediately, including a deploy that was already in flight. Dry-run mode evaluates the policy and logs what it would have denied, without denying anything:
name: organizations/123456789012/policies/compute.vmExternalIpAccess
dryRunSpec:
rules:
- denyAll: true
Violations appear in Cloud Logging with dry_run: true, which gives you the list of things that will break before they break:
gcloud logging read \
'protoPayload.metadata.dryRun=true AND protoPayload.status.code!=0' \
--limit=50 --format='table(resource.labels.project_id, protoPayload.methodName)'
The rollout that works:
1. Set the constraint in dryRunSpec at the organisation.
2. Wait two weeks — long enough to include a monthly job and a release.
3. Read the violation log. Every entry is either a thing to fix or an exception to write.
4. Fix and document. Then move the rule from dryRunSpec to spec.
5. Keep the dry-run entry for the next constraint.
Skipping step 2 is how a well-intentioned guardrail becomes an outage, and it is the reason security teams get a reputation for breaking things.
Topic 5: Custom Constraints
Predefined constraints cover common cases. Custom constraints express organisation-specific rules against any resource field:
name: organizations/123456789012/customConstraints/custom.gkeRequirePrivateNodes
resourceTypes:
- container.googleapis.com/Cluster
methodTypes: [CREATE, UPDATE]
condition: "resource.privateClusterConfig.enablePrivateNodes == true"
actionType: ALLOW
displayName: "GKE clusters must have private nodes"
description: "Public node IPs are not permitted; use Cloud NAT for egress."
gcloud org-policies set-custom-constraint custom-constraint.yaml
gcloud org-policies set-policy enforce-custom.yaml --organization=123456789012
The condition language is CEL against the resource, which means you can enforce almost any field-level rule that a reviewer would otherwise have to catch by reading Terraform.
Where custom constraints stop being the right tool: anything requiring context beyond the resource itself — “only during a change window”, “only if the ticket is approved”. That is policy-as-code in the pipeline (the CI/CD module’s supply-chain lesson), not an org policy.
Topic 6: The Rest of the Guardrail Layer
Org policy is one of four org-level controls, and they are complementary rather than alternatives:
Essential Contacts — who Google emails about security, suspension and technical incidents. Left unset, those notices go to whoever created the billing account, who may have left.
gcloud essential-contacts create --email=security@acme.example \
--notification-categories=SECURITY,SUSPENSION --organization=123456789012
Budgets and billing alerts on every project, with a Pub/Sub notification rather than only email. A project with no budget has no smoke detector — and in a sandbox folder, a budget wired to a function that disables billing is the difference between a lesson and an invoice.
Security Command Center for posture: misconfigurations, public buckets, over-permissive bindings, and vulnerability findings across the organisation. The Standard tier is free and catches the well-known mistakes.
Cloud Asset Inventory for “what actually exists”, which is the question no console page answers well:
# Every external IP in the organisation, right now
gcloud asset search-all-resources --scope=organizations/123456789012 \
--asset-types=compute.googleapis.com/Instance \
--query='natIP:*' --format='table(name, location, additionalAttributes)'
Asset Inventory also feeds a BigQuery export, which is how you answer compliance questions with a query instead of a spreadsheet.
Try it yourself: set compute.vmExternalIpAccess to deny in dry-run at the org level and read the violation log after a day. Almost every organisation finds a VM nobody knew had a public IP — which is the guardrail proving its value before it has blocked anything.
Common mistake: enforcing gcp.resourceLocations on a folder that already contains resources outside the allowed regions. The policy does not delete them; it makes them unmanageable — you cannot modify or recreate them, and they keep billing. Inventory with Asset Inventory first, migrate or delete, then enforce. In that order, always.