Organization Policy & Guardrails

Constraints that cap what any project may do, why an org policy grants nothing, and how to roll one out in dry-run before it blocks a production deploy.

intermediate 22 min lesson hands-on task included

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

ORG POLICY SETS THE CEILING — IAM GRANTS INSIDE IT Organization constraints/compute.requireOsLogin = true Folder: Production constraints/gcp.resourceLocations = eu only Project: checkout-prod inherits both, cannot loosen either Folder: Sandbox allowedPolicyMemberDomains + budget cap ORG POLICY what MAY exist IAM who MAY act EFFECTIVE the overlap only AN ORG POLICY GRANTS NOTHING It only removes options. A project owner with roles/owner still cannot create a VM in a region the policy excludes. TEST BEFORE YOU ENFORCE gcloud resource-manager org-policies describe … --effective · dry-run mode reports violations before it blocks anything
Constraints inherit downward and intersect with IAM. The banner in the middle is the sentence that resolves most confusion — an org policy can only remove options, never grant them.

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:

ConstraintPrevents
compute.vmExternalIpAccessA VM appearing directly on the internet
iam.disableServiceAccountKeyCreationLong-lived JSON keys leaving the platform
iam.allowedPolicyMemberDomainsGranting access to a personal Gmail account
storage.publicAccessPreventionA publicly readable bucket, ever
sql.restrictPublicIpA Cloud SQL instance with a public address
compute.requireShieldedVmBooting an image without integrity monitoring
gcp.resourceLocationsData created outside your permitted regions
compute.disableSerialPortAccessAn unaudited console path into a VM
compute.restrictVpcPeeringA 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 enforce with 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.