Home / Security

GCP Organization Policies to Enable by Default

GCP Organization Policies to Enable by Default

Every GCP organization should enforce a baseline set of organization policies by default: disable service account key creation, restrict IAM members to your Workspace domains, skip default network creation, require shielded VMs, block VM external IPs, forbid public Cloud SQL IPs, and enforce uniform bucket-level access with public access prevention. Managing them as code through one Terraform module built on google_org_policy_policy makes the baseline versioned, auditable, and impossible to weaken by accident.

By Mateusz Chmielewski · Aug 4, 2026 · 18 min read

What Are GCP Organization Policies?

Google Cloud Organization Policies are guardrails configured at the organization, folder, or project level that restrict how resources can be configured — independent of who has IAM permission to configure them. While IAM answers 'who can do what', org policies answer 'what is allowed at all': even a Project Owner cannot create a service account key if the iam.managed.disableServiceAccountKeyCreation constraint is enforced. Managing them with the Terraform google_org_policy_policy resource turns your governance baseline into versioned, reviewable code.

Think of IAM as the badge reader deciding who may enter each room of a building. Organization policies are the building code itself: no matter whose badge opens the door, nobody is allowed to remove the fire doors or wire the sprinklers to a home battery. A Terraform module is the architect's stamped blueprint — the code of the building code, applied to every floor by default.

ConceptExplanationWhen to use
ConstraintA named restriction defined by Google, e.g. iam.managed.disableServiceAccountKeyCreation.Choosing which behavior to govern; constraints are fixed by Google, you only configure them.
Boolean PolicyA constraint that is simply enforced or not (TRUE/FALSE).Binary guardrails like blocking serial port access or default networks.
List PolicyA constraint with explicit allowed_values or denied_values.Restricting domains, external IPs, or locations to a known set.
Hierarchy InheritancePolicies set at org level flow down to folders and projects unless overridden.Default-deny at the org node with narrow, documented exceptions below.
Managed (v2) ConstraintsNewer constraint names like iam.managed.* used by google_org_policy_policy.All new Terraform code; the legacy google_organization_policy resource is deprecated in practice.

Why Manage Organization Policies as Code in a Terraform Module?

In most organizations, org policies are clicked together in the Cloud Console once, by one admin, and never reviewed again. Nobody knows which constraints are enforced, exceptions are invisible, and a well-meaning admin can silently relax a guardrail that was protecting 200 projects. New folders and projects inherit whatever drifted state exists — or worse, no policies at all.

A Terraform module that owns the entire baseline flips governance into an auditable pipeline: every constraint, value, and exception lives in Git, changes require code review, and terraform plan shows exactly which guardrail would change before it does. Enforced policies block misconfiguration even from Org Admins, which pairs naturally with [VPC Service Controls perimeters](/tutorial/terraform-vpc-service-controls-custom-module-guide) for data exfiltration defense, [Secret Manager Automatic Rotation](/tutorial/gcp-secret-manager-terraform-module-automatic-rotation) for credential lifecycle, [Low-Cost 3-Tier Firebase Hardening](/tutorial/firebase-3-tier-app-low-cost-security-hardening) for serverless app security, and [IAM Recommender least-privilege fixes](/tutorial/fix-overprivileged-iam-policy-analyzer-recommender) for trimming existing access.

FeaturethisServicealtAaltB
Policy AuditabilityFull history in Git + terraform plan diffsConsole clicks: no trail of intentgcloud scripts: run once, then forgotten
Exception HandlingExplicit folder overrides in code, reviewableSilent console edits by any adminHard-coded per-project script forks
Consistency at ScaleOne module, same baseline on every org/folderDrift between environmentsPer-team reinvention, conflicting values
Rollback Speedgit revert + terraform apply in minutesManual reconstruction from memoryRe-run old scripts and hope state matches

Prerequisites

  • GCP Organization with at least one folder and project
  • Organization Policy Administrator role (`roles/orgpolicy.policyAdmin`) on the org node
  • gcloud CLI v450.0+ installed and authenticated
  • Terraform CLI v1.5.0+ with the google provider ~> 5.0
  • Your Cloud Identity customer ID(s) for domain-restricted sharing (from `gcloud organizations list` / Workspace admin console)

Step-by-Step Guide

Step 1: Enable Required APIs and Get Your Organization ID

Activating the Org Policy and Resource Manager APIs in your admin project, and capturing the organization ID used as the policy parent. Policies are attached to hierarchy nodes by name (organizations/ORG_ID), so every resource in the module depends on these identifiers being correct from the start.

gcloud services enable orgpolicy.googleapis.com \
  cloudresourcemanager.googleapis.com \
  --project=my-admin-project

gcloud organizations list

Step 2: Define Module Inputs (variables.tf)

Declaring the module contract: the parent node and a map of policies where each entry describes one constraint, its type, and its values. A typed policy map lets one module enforce every constraint through a single for_each — adding a new guardrail becomes a one-line data change instead of a new resource block.

variable "parent" {
  description = "Hierarchy node to attach policies to, e.g. organizations/123456789012 or folders/456"
  type        = string
}

variable "policies" {
  description = "Map of organization policies to enforce"
  type = map(object({
    constraint     = string           # e.g. iam.managed.disableServiceAccountKeyCreation
    policy_type    = string           # "boolean" | "list"
    enforce        = optional(bool, true)
    allowed_values = optional(list(string), [])
    denied_values  = optional(list(string), [])
  }))

  validation {
    condition     = alltrue([for p in var.policies : contains(["boolean", "list"], p.policy_type)])
    error_message = "policy_type must be 'boolean' or 'list'."
  }
}

Step 3: Implement the Policy Resource (main.tf)

One google_org_policy_policy resource with for_each that renders boolean rules, list rules, and value handling per constraint. google_org_policy_policy is the modern Org Policy API v2 resource; it supports managed constraints and clean per-rule semantics that the legacy google_organization_policy resource lacks.

resource "google_org_policy_policy" "this" {
  for_each = var.policies

  name   = "${var.parent}/policies/${each.value.constraint}"
  parent = var.parent

  spec {
    # Boolean constraints: enforce = TRUE/FALSE
    dynamic "rules" {
      for_each = each.value.policy_type == "boolean" ? [1] : []
      content {
        enforce = each.value.enforce ? "TRUE" : "FALSE"
      }
    }

    # List constraints: allowed and/or denied values
    dynamic "rules" {
      for_each = each.value.policy_type == "list" ? [1] : []
      content {
        values {
          allowed_values = length(each.value.allowed_values) > 0 ? each.value.allowed_values : null
          denied_values  = length(each.value.denied_values) > 0 ? each.value.denied_values : null
        }
      }
    }
  }
}

Step 4: Ship the Golden Defaults Baseline (defaults.tfvars)

The curated set of 10 organization policies every GCP org should enable by default, expressed as plain data for the module. This is the security core of the article: these constraints close the misconfiguration classes behind the most common real-world GCP incidents — leaked service account keys, external sharing, and public database IPs.

parent = "organizations/123456789012"

policies = {
  # 1. No service account keys — the single highest-impact guardrail
  disable_sa_key_creation = {
    constraint  = "iam.managed.disableServiceAccountKeyCreation"
    policy_type = "boolean"
  }
  # 2. Block uploading externally-minted keys to service accounts
  disable_sa_key_upload = {
    constraint  = "iam.managed.disableServiceAccountKeyUpload"
    policy_type = "boolean"
  }
  # 3. IAM members only from your Workspace domains (domain-restricted sharing)
  allowed_policy_member_domains = {
    constraint     = "iam.allowedPolicyMemberDomains"
    policy_type    = "list"
    allowed_values = ["is:C0123abcd"] # your Cloud Identity customer ID
  }
  # 4. No default VPC with its permissive firewall rules
  skip_default_network = {
    constraint  = "compute.skipDefaultNetworkCreation"
    policy_type = "boolean"
  }
  # 5. Require Shielded VMs (vTPM + integrity monitoring)
  require_shielded_vm = {
    constraint  = "compute.requireShieldedVm"
    policy_type = "boolean"
  }
  # 6. No interactive serial console access to VMs
  disable_serial_port = {
    constraint  = "compute.disableSerialPortAccess"
    policy_type = "boolean"
  }
  # 7. Deny external IPs on all VM instances
  deny_vm_external_ip = {
    constraint    = "compute.vmExternalIpAccess"
    policy_type   = "list"
    denied_values = ["*"]
  }
  # 8. No public IPs on Cloud SQL instances
  sql_restrict_public_ip = {
    constraint  = "sql.restrictPublicIp"
    policy_type = "boolean"
  }
  # 9. Uniform bucket-level access (no object ACLs)
  storage_uniform_access = {
    constraint  = "storage.uniformBucketLevelAccess"
    policy_type = "boolean"
  }
  # 10. Public access prevention on all new buckets
  storage_public_access_prevention = {
    constraint    = "storage.publicAccessPrevention"
    policy_type   = "list"
    allowed_values = ["enforced"]
  }
}

Step 5: Add Auditable Folder-Level Exceptions (exceptions.tf)

Creating narrow overrides at specific folders for workloads that genuinely need a relaxed guardrail — e.g., one legacy folder where VMs need external IPs. Exceptions are unavoidable; the goal is to make each one explicit, scoped to the smallest node, and visible in code review instead of buried in the console.

# Exception: the 'legacy' folder may assign external IPs to two allowlisted VMs.
# Ticket: SEC-1042 — migration to internal-only LB scheduled Q3.
resource "google_org_policy_policy" "legacy_external_ip_exception" {
  name   = "folders/987654321/policies/compute.vmExternalIpAccess"
  parent = "folders/987654321"

  spec {
    inherit_from_parent = true

    rules {
      values {
        allowed_values = [
          "projects/legacy-app/instances/bastion-01",
          "projects/legacy-app/instances/nat-egress-01",
        ]
      }
    }
  }
}

Step 6: Expose Outputs and Apply (outputs.tf)

Returning the managed policy names for audit tooling, then running the apply. Downstream compliance scanners and documentation generators can enumerate exactly which constraints this module owns without parsing Terraform state directly.

output "enforced_policies" {
  description = "Map of policy key to fully-qualified policy resource name"
  value       = { for k, p in google_org_policy_policy.this : k => p.name }
}

output "parent_node" {
  description = "Hierarchy node the baseline is attached to"
  value       = var.parent
}

# Apply:
# terraform init
# terraform plan -var-file=defaults.tfvars
# terraform apply -var-file=defaults.tfvars

Verification & Health Check

Best Practices

  • Enforce at the Org Node, Not Per Project
  • Deny All, Then Allowlist
  • Use Managed (v2) Constraint Names
  • Scope Exceptions to Folders, Never the Org
  • Test Baseline Changes in Dry-Run First

Common Mistakes

  • {"errorCode":"CONSTRAINT_NOT_FOUND","symptoms":"Apply fails with `Constraint ... does not exist` for a policy name that looks correct.","rootCause":"Legacy and managed constraint names are different identifiers — iam.disableServiceAccountKeyCreation vs iam.managed.disableServiceAccountKeyCreation. The wrong one 404s in the v2 API.","fixCommand":"gcloud org-policies list-custom-constraints --organization=123456789012; check Google's constraint reference for the exact managed name","code":"# Wrong (legacy name with v2 resource):\nname = \"organizations/123/policies/iam.disableServiceAccountKeyCreation\"\n\n# Right (managed name):\nname = \"organizations/123/policies/iam.managed.disableServiceAccountKeyCreation\"\n","language":"hcl","filename":"main.tf","prevention":"Copy constraint names verbatim from the official org-policy-constraints list into the module's tfvars — never type them from memory."}
  • {"errorCode":"FAILED_PRECONDITION","symptoms":"Error 400: `must specify exactly one of enforce or values` when applying a policy.","rootCause":"A boolean constraint was given a values block, or a list constraint was given an enforce flag — the module's policy_type did not match the real constraint type.","fixCommand":"terraform plan -var-file=defaults.tfvars 2>&1 | grep policy_type","code":"# compute.requireShieldedVm is boolean — this fails:\nrules {\n values {\n allowed_values = [\"TRUE\"]\n }\n}\n\n# Correct:\nrules {\n enforce = \"TRUE\"\n}\n","language":"hcl","filename":"main.tf","prevention":"Keep a comment with each constraint's type in defaults.tfvars and validate with a small apply against a test folder before the org node."}
  • {"errorCode":"POLICY_CONFLICT_ON_DELETE","symptoms":"After terraform destroy, workloads are still blocked by the 'deleted' policy.","rootCause":"Destroying the Terraform resource removes the custom policy, but a constraint may still be enforced by an inherited policy from a higher node — or you expected 'not set' but actually need an explicit restore-to-default.","fixCommand":"gcloud org-policies describe CONSTRAINT --organization=ORG_ID","code":"# To explicitly restore Google-managed default behavior at a node:\nresource \"google_org_policy_policy\" \"reset_example\" {\n name = \"${var.parent}/policies/compute.requireShieldedVm\"\n parent = var.parent\n\n spec {\n rules {\n # No enforce/values — restores the default at this node\n }\n }\n}\n","language":"hcl","filename":"main.tf","prevention":"Remember org policies inherit downward: always check the full hierarchy with gcloud org-policies describe at the project level before assuming a policy is gone."}
  • {"errorCode":"PERMISSION_DENIED","symptoms":"Apply fails with `The caller does not have permission` on org-level policy resources.","rootCause":"roles/orgpolicy.policyAdmin was granted on a project or folder, but the policy targets the organization node — roles must exist at the same node or higher.","fixCommand":"gcloud organizations add-iam-policy-binding 123456789012 --member='serviceAccount:[email protected]' --role='roles/orgpolicy.policyAdmin'","code":"resource \"google_organization_iam_member\" \"policy_admin\" {\n org_id = \"123456789012\"\n role = \"roles/orgpolicy.policyAdmin\"\n member = \"serviceAccount:[email protected]\"\n}\n","language":"hcl","filename":"iam.tf","prevention":"Bootstrap the automation identity's org-level roles in a separate, manually-applied root workspace before the policy module ever runs."}
  • {"errorCode":"SHARING_BLOCKED_UNEXPECTEDLY","symptoms":"After enabling domain-restricted sharing, legitimate external partner access breaks with IAM policy update errors.","rootCause":"iam.allowedPolicyMemberDomains rejected a partner's domain because only your own Cloud Identity customer ID was allowlisted.","fixCommand":"terraform plan -var-file=defaults.tfvars # with partner customer ID added","code":"allowed_policy_member_domains = {\n constraint = \"iam.allowedPolicyMemberDomains\"\n policy_type = \"list\"\n allowed_values = [\n \"is:C0123abcd\", # your org\n \"is:C09wxyz88\", # audited partner org\n ]\n}\n","language":"hcl","filename":"defaults.tfvars","prevention":"Inventory existing external IAM members (via Cloud Asset Inventory search-all-iam-policies) before enforcing the constraint, and allowlist partner domains in the same change."}

Cost Analysis

Featuremetriccost1kcost10kcost100kcost1m
Organization Policy Service$0.00$0.00$0.00$0.00
Admin Activity Audit Logs (policy changes)$0.00$0.00$0.00$0.00
Terraform State Storage (GCS, <1 MB)~$0.02~$0.02~$0.02~$0.02
CI/CD Pipeline Runs (plan/apply, Cloud Build free tier)$0.00$0.00$0.00$0.00

References

Browse all tutorials