Chapter 8: Identity & Access on GCP
Topic 54

Org Policies as Code

GovernanceGuardrails

Org policies are the other half of GCP governance, and they answer a different question than IAM. IAM controls who can act; an org policy controls what can be created at all, regardless of who is asking. It is a guardrail that constrains the shape of resources across the hierarchy — "no VM in this org may have an external IP" — rather than a grant to a principal.

google_org_policy_policy applies a constraint like "no external IPs," "only these regions," or "OS Login required" at the org, a folder, or a project. Like IAM, it inherits down the tree: a constraint set on hatch.io binds every folder and project beneath it. The key property is that IAM cannot override it — a user with full roles/owner still cannot create what a constraint forbids. That is the entire point.

Orthogonal to IAM

IAM is the who: this group may create VMs. An org policy is the what: no VM in this org may have an external IP, even for someone with permission to create VMs. The two are orthogonal, and a user with complete IAM access still cannot violate an org policy. That is what makes org policies a real guardrail — they are the controls the project owner cannot click past, because no amount of IAM grants what a constraint denies.

This is the mental model to fix first, because the instinct from IAM is to "give someone permission" to do the blocked thing. There is no such permission. The only way past a constraint is to change the policy at a node where you are allowed to, which is itself an audited Terraform change — not a runtime grant.

Two orthogonal layers — you need both
IAM — the WHO
Who may act: this group may create VMs. Additive, inherited, loosened by granting more. Answers who is allowed.
Org policy — the WHAT
What may exist at all: no VM may have an external IP, even for an owner. Unbypassable — IAM cannot override it.

The Resource and Constraints

google_org_policy_policy targets a node — organizations/123456789012, a folder, or a project — and sets a named constraint. The common ones disable external IPs (compute.vmExternalIpAccess), restrict resource locations (gcp.resourceLocations), and require OS Login (compute.requireOsLogin). The configuration below denies external IPs across the whole org, so a public IP can never be attached to a VM anywhere under hatch.io.

A list constraint denying external IPs org-wide
resource "google_org_policy_policy" "no_external_ip" {
  name   = "organizations/123456789012/policies/compute.vmExternalIpAccess"
  parent = "organizations/123456789012"

  spec {
    rules {
      deny_all = "TRUE"   # list constraint: empty allow-list, inherited by every project
    }
  }
}

Boolean vs List Constraints

Constraints come in two shapes. Boolean constraints are on/off — require OS Login is either enforced or it is not. List constraints allow or deny a set of values — allow only us-central1 and us-east1 as regions, or deny service-account key creation. The rules {} block expresses the allow/deny lists and the enforcement, and the difference matters when you write one: a boolean constraint uses enforce, a list constraint uses allow_all, deny_all, or explicit values.

A list constraint is where the sharpest self-inflicted outage lives. Write a gcp.resourceLocations allow-list of regional values and forget that Cloud Storage and BigQuery often use multi-regions like US, and bucket and dataset creation breaks across the org — the multi-region value is not in your allow-list, so it is denied. Test list constraints before they reach the org node.

Inheritance and Override

A policy set at hatch.io applies to all of platform/, apps/, and data/. Depending on the constraint's inheritance behavior, a child node can be allowed to override or merge — so the pattern is to set the strict default high and relax it deliberately where a project genuinely needs the exception. Deny external IPs at the org, then, in the one project that hosts a bastion, set a project-level policy that allows the bastion's IP. You design the exception at the node that needs it instead of weakening the org-wide default.

Guardrails That Constrain Creation

These are the controls that stop a public IP from ever being attached in hatch-app-prod, or stop a resource landing outside approved regions, before the risky resource exists. That is prevention, not detection — the difference between a constraint that refuses the terraform apply and a scanner that files a ticket after the fact. A guardrail that blocks creation closes the window in which the misconfiguration exists at all.

Org policies are not a substitute for IAM least-privilege; they are the complement. IAM is still the layer that decides who may create a VM at all, and an org policy decides what that VM may look like. Skip either and you leave a gap the other was meant to cover. The closing topic of the chapter turns from access to the metadata that classifies resources — labels, tags, and network tags.

Org Policy vs IAM Grant

IAM grant — the who: this group may create or edit this kind of resource. Additive, inherited, overridable by granting more. Answers "who is allowed to act."

Org policy — the what: this kind of resource may not be created in this shape, regardless of who asks. Cannot be bypassed by IAM, even by an org owner. Answers "what is allowed to exist." You need both; each catches what the other misses.

Common Mistakes
  • Assuming an org policy is an IAM grant and trying to "give someone permission" to bypass it — org policies cannot be overridden by IAM; even an org owner cannot create what a constraint forbids without changing the policy.
  • Setting compute.vmExternalIpAccess to deny at the org and then being unable to create a needed bastion's external IP in one project — design the override at that project instead of fighting the inherited default.
  • Writing a gcp.resourceLocations allow-list that omits the multi-regions storage and BigQuery use (US) and breaking bucket and dataset creation across the org.
  • Treating org policies as the only governance layer and skipping IAM least-privilege — they are orthogonal; you need both the who and the what.
  • Clicking org policies into the Console instead of managing them in Terraform — the guardrails then drift, unreviewed, exactly the resources you most want version-controlled.
Best Practices
  • Set strict guardrails high in the hierarchy — deny external IPs, restrict regions, require OS Login at hatch.io — and grant deliberate per-node exceptions only where a project needs one.
  • Manage org policies in Terraform with google_org_policy_policy so the guardrails are reviewed and version-controlled alongside IAM, not clicked in the Console.
  • Pair every org policy with IAM least-privilege — constrain what can be created and who can create it, because each catches what the other misses.
  • Test a new constraint on a folder like apps/ before applying it at the org node, so an over-broad allow or deny list does not break creation org-wide.
  • Include the multi-region values your storage and analytics services need (US) in any gcp.resourceLocations allow-list, not only regional ones.
Comparable models AWS Service Control Policies (SCPs) the direct analog as account-level guardrails, though SCPs are deny-based JSON Azure Policy the closest Azure equivalent Config Connector can manage the same org policies via KRM

Knowledge Check

What is the core difference between an org policy and an IAM grant?

  • An IAM grant controls who can act; an org policy controls what can be created at all, regardless of who is asking
  • An org policy grants permissions to principals; an IAM grant constrains which resource shapes and configurations can be created
  • They are two names for the same construct applied at different nodes
  • An org policy applies only to service accounts, whereas an IAM grant applies only to human user accounts

A user with roles/owner on a project tries to create a VM with an external IP, but the org has compute.vmExternalIpAccess denied. What happens?

  • Creation is refused — IAM cannot override an org policy, so even an owner cannot create what the constraint forbids
  • It succeeds, because roles/owner on the project carries an implicit permission that lets the holder bypass any inherited org policy
  • It succeeds in the project but the IP is stripped on the next org sync
  • The org policy is automatically relaxed because an owner requested it

Why might a gcp.resourceLocations allow-list of only regional values break the org?

  • Storage and BigQuery often use multi-regions like US; if the multi-region value is not allowed, bucket and dataset creation is denied
  • Regional values are deprecated, so the whole constraint is ignored
  • List constraints can only deny values and never allow them, so configuring an allow-list silently blocks every resource location instead
  • Allow-lists apply only to VMs, so storage is unaffected and nothing breaks

You deny external IPs at hatch.io but one project needs a bastion with a public IP. What is the right approach?

  • Set a project-level org policy that allows the exception, rather than weakening the org-wide default
  • Grant the bastion's service account roles/owner so it can bypass the org policy
  • Remove the org-level constraint entirely and re-add it on every project node except the one bastion project that needs the public IP
  • Use an IAM condition on the VM to override the org policy for that resource

You got correct