Cloud SecurityGoogle CloudTerraform

Top 10 Organization Policy Constraints for Google Cloud

Jorge Liauw Calo
Jorge Liauw CaloSecurity Engineer @ Google Cloud
•7 min read
Top 10 Organization Policy Constraints for Google Cloud

Apply the right guardrails to keep your Cloud workloads secure.

When building in Google Cloud, you need technical boundaries that enforce your security and compliance baseline automatically. Just as an office building uses badge access to restrict who can enter sensitive rooms, Google Cloud Organization Policy Service gives you centralized, preventive guardrails across every project in your organization.

How It Looks in Practice: Hierarchy & Inheritance

To get the most out of Organization Policies, you apply your baseline constraints at the Organization node so they are automatically inherited by every Folder and Project underneath:

Organization (yourcompany.com)  <-- Apply baseline Top 10 constraints here
├── Folder: Production
│   ├── Project: webshop-prod
│   └── Project: payments-prod
├── Folder: Non-Production
│   ├── Project: webshop-dev
│   └── Project: sandbox-team-a
└── Folder: Shared Services
    └── Project: network-hub    <-- Scope exceptions with Resource Manager Tags

From Top 5 to Top 10: What Changed?

Back in early 2024, I published my original Top 5 Organization Policy Constraints for Google Cloud. Two important things have changed since then:

  1. Secure-by-default for new organizations: Organizations created on or after May 3, 2024 get a baseline set of constraints enforced automatically (such as disabling SA key creation and enforcing uniform bucket-level access). However, if your organization was created before May 2024, these guardrails are not enabled retroactively—you must apply them yourself.
  2. Managed constraints (*.managed.*): Newer managed constraints run on the same engine as custom constraints, which means they support dry-run mode (dry_run_spec) and the Organization Policy Simulator for safe rollouts.

Terraform Setup

All examples below use the v2 google_org_policy_policy resource (roles/orgpolicy.policyAdmin required at the organization level):

variable "org_id" {
  description = "Your Google Cloud organization ID"
  type        = string
}

locals {
  org = "organizations/${var.org_id}"
}

1. Resource Location Restriction

Constraint: gcp.resourceLocations

Limits the physical regions where new cloud resources can be deployed—essential for GDPR, data residency, digital sovereignty, and low-latency / low-carbon region selection. Use Google-curated value groups like in:eu-locations (all EU regions) or in:europe-west4-locations so new zones are covered automatically.

resource "google_org_policy_policy" "resource_locations" {
  name   = "${local.org}/policies/gcp.resourceLocations"
  parent = local.org

  spec {
    rules {
      values {
        allowed_values = ["in:eu-locations"]
      }
    }
  }
}

Note: Global resources are unaffected; check the supported services list for coverage.

2. Domain Restricted Sharing

Constraint: iam.managed.allowedPolicyMembers (or legacy iam.allowedPolicyMemberDomains)

Ensures only identities from your organization can be granted IAM roles, preventing accidental data sharing with personal Gmail accounts, former contractors, or allUsers / allAuthenticatedUsers. Unlike the legacy constraint, iam.managed.allowedPolicyMembers lets you reference your org_id directly and allowlist specific external partner accounts without opening an entire external domain.

resource "google_org_policy_policy" "allowed_policy_members" {
  name   = "${local.org}/policies/iam.managed.allowedPolicyMembers"
  parent = local.org

  spec {
    rules {
      enforce = "TRUE"
      parameters = jsonencode({
        allowedPrincipalSets = [
          "//cloudresourcemanager.googleapis.com/organizations/${var.org_id}"
        ]
      })
    }
  }
}

3. Disable Service Account Key Creation and Upload

Constraints: iam.managed.disableServiceAccountKeyCreation & iam.managed.disableServiceAccountKeyUpload

Exported service account JSON keys do not expire by default and frequently leak in Git repos or laptops. Instead of static keys, use Workload Identity Federation for external CI/CD pipelines (GitHub Actions, GitLab, AWS/Azure), attached service accounts on GCP workloads, and service account impersonation for local development.

resource "google_org_policy_policy" "disable_sa_key_creation" {
  name   = "${local.org}/policies/iam.managed.disableServiceAccountKeyCreation"
  parent = local.org

  spec {
    rules {
      enforce = "TRUE"
    }
  }
}

resource "google_org_policy_policy" "disable_sa_key_upload" {
  name   = "${local.org}/policies/iam.managed.disableServiceAccountKeyUpload"
  parent = local.org

  spec {
    rules {
      enforce = "TRUE"
    }
  }
}

4. Define Allowed External IP Addresses for VM Instances

Constraint: compute.vmExternalIpAccess (or compute.managed.vmExternalIpAccess)

By default, any Compute Engine VM can be assigned a public IP. Denying external IPs across your organization shrinks your internet attack surface—teams can use Cloud NAT for outbound traffic, Identity-Aware Proxy (IAP) for SSH/RDP, and Cloud Load Balancing with Cloud Armor for ingress.

resource "google_org_policy_policy" "vm_external_ip_access" {
  name   = "${local.org}/policies/compute.vmExternalIpAccess"
  parent = local.org

  spec {
    rules {
      deny_all = "TRUE"
    }
  }
}

(If specific appliances need a public IP, list their instance paths in values.allowed_values or exempt them with Tags.)

5. Enforce Public Access Prevention and Uniform Bucket-Level Access

Constraints: storage.publicAccessPrevention & storage.uniformBucketLevelAccess

Public Access Prevention blocks Cloud Storage buckets and objects from ever being exposed to allUsers or allAuthenticatedUsers (and applies retroactively to existing buckets). Pair it with Uniform Bucket-Level Access to disable legacy object-level ACLs so all bucket permissions are governed strictly through Cloud IAM.

resource "google_org_policy_policy" "public_access_prevention" {
  name   = "${local.org}/policies/storage.publicAccessPrevention"
  parent = local.org

  spec {
    rules {
      enforce = "TRUE"
    }
  }
}

resource "google_org_policy_policy" "uniform_bucket_level_access" {
  name   = "${local.org}/policies/storage.uniformBucketLevelAccess"
  parent = local.org

  spec {
    rules {
      enforce = "TRUE"
    }
  }
}

6. Define Trusted Image Projects

Constraint: compute.trustedImageProjects

Restricts which projects teams can pull Compute Engine boot disk images from, ensuring VMs only run hardened “golden images” or approved OS distributions while keeping licensing costs predictable.

resource "google_org_policy_policy" "trusted_image_projects" {
  name   = "${local.org}/policies/compute.trustedImageProjects"
  parent = local.org

  spec {
    rules {
      values {
        allowed_values = [
          "projects/your-golden-images",
          "projects/debian-cloud",
          "projects/cos-cloud",
          "projects/gke-node-images",
          "projects/ubuntu-os-gke-cloud",
        ]
      }
    }
  }
}

Note: Include projects/gke-node-images and projects/ubuntu-os-gke-cloud if you run Google Kubernetes Engine (GKE).

7. Skip Default Network Creation

Constraint: compute.skipDefaultNetworkCreation

Prevents new projects from automatically provisioning the default VPC network and its permissive 0.0.0.0/0 SSH (22) and RDP (3389) firewall rules, so your platform team can provision segmented networks (such as Shared VPC) instead.

resource "google_org_policy_policy" "skip_default_network" {
  name   = "${local.org}/policies/compute.skipDefaultNetworkCreation"
  parent = local.org

  spec {
    rules {
      enforce = "TRUE"
    }
  }
}

8. Require OS Login

Constraint: compute.managed.requireOsLogin

Replaces static SSH keys in project/instance metadata with IAM-backed Linux SSH authentication (roles/compute.osLogin / roles/compute.osAdminLogin), enabling 2FA and per-user audit logging. See also my post on blocking project-wide SSH keys.

resource "google_org_policy_policy" "require_os_login" {
  name   = "${local.org}/policies/compute.managed.requireOsLogin"
  parent = local.org

  spec {
    rules {
      enforce = "TRUE"
    }
  }
}

9. Restrict Public IP Access on Cloud SQL Instances

Constraint: sql.managed.restrictPublicIp

Prevents users from enabling public IPv4 addresses on Cloud SQL databases, ensuring database traffic stays on private networks via Private Service Access or Private Service Connect.

resource "google_org_policy_policy" "sql_restrict_public_ip" {
  name   = "${local.org}/policies/sql.managed.restrictPublicIp"
  parent = local.org

  spec {
    rules {
      enforce = "TRUE"
    }
  }
}

10. Disable VM Serial Port Access

Constraint: compute.managed.disableSerialPortAccess

Blocks interactive serial console logins (serial-port-enable=true) that could otherwise bypass VPC firewall rules.

resource "google_org_policy_policy" "disable_serial_port_access" {
  name   = "${local.org}/policies/compute.managed.disableSerialPortAccess"
  parent = local.org

  spec {
    rules {
      enforce = "TRUE"
    }
  }
}

Tip: Group Boolean Guardrails with for_each in Terraform

Instead of repeating a separate resource block for every boolean policy, you can deploy all of them cleanly with for_each (and optionally add iam.automaticIamGrantsForDefaultServiceAccounts, compute.managed.restrictProtocolForwardingCreationForTypes, and essentialcontacts.managed.allowedContactDomains):

locals {
  boolean_constraints = toset([
    "iam.managed.disableServiceAccountKeyCreation",
    "iam.managed.disableServiceAccountKeyUpload",
    "iam.automaticIamGrantsForDefaultServiceAccounts",
    "storage.publicAccessPrevention",
    "storage.uniformBucketLevelAccess",
    "compute.skipDefaultNetworkCreation",
    "compute.managed.requireOsLogin",
    "compute.managed.disableSerialPortAccess",
    "sql.managed.restrictPublicIp",
  ])
}

resource "google_org_policy_policy" "boolean_guardrails" {
  for_each = local.boolean_constraints
  name     = "${local.org}/policies/${each.value}"
  parent   = local.org

  spec {
    rules {
      enforce = "TRUE"
    }
  }
}

How to Roll Out Constraints Safely

Enforcing organization-wide policies without testing can break existing CI/CD pipelines. Follow a three-step rollout:

  1. Audit first with dry_run_spec: Replace spec with dry_run_spec in Terraform to evaluate the policy in audit-only mode first; any action that would have been denied is logged in Cloud Audit Logs without blocking deployments.
  2. Check existing resources with Policy Simulator: Run gcloud policy-intelligence simulate orgpolicy --organization=ORG_ID --policies=policy.yaml to see which existing resources would violate a proposed constraint.
  3. Scope exceptions with Resource Manager Tags: When a specific project or VM needs an exception, keep the policy enforced at the organization level and add a conditional rule matching a Tag (expression = "resource.matchTag('${var.org_id}/exception', 'allowed')" with enforce = "FALSE") so every exception stays auditable in one place.

Keep in mind that most Organization Policies are not retroactive (except storage.publicAccessPrevention)—pair them with Security Command Center and real-time Cloud Asset Inventory notifications to detect existing drift!

Curious to explore more constraints? Check out the official Organization Policy constraints list and follow me on LinkedIn!

Jorge Liauw Calo
Written By

Jorge Liauw Calo

Security Engineer at Google Cloud (Google Cybershield) with 13+ years of experience in Cybersecurity across highly regulated industries including Semiconductor, Banking, Fintech, and Insurance. Active member of the Google Cloud Community BeNeLux and Google Cloud Security Community Amsterdam.