Org policy & landing zones
Preventive constraints and secure-by-default projects.
Hand someone the Owner role on a Google Cloud project and you have handed them a lot of rope. They can grant themselves roles/editor without asking anyone. They can boot a VM (a virtual machine, which is a rented computer running inside Google's data centre) with a public IP address facing the open internet. They can download a service-account key (a small password file that lets an app log in as a robot user) that never expires and ends up sitting in a folder on someone's laptop. Detection will catch all three. It catches them afterwards, though, usually once the key has already been pasted into a git repository. The stronger move is to make those three actions impossible, at a level of the hierarchy the Owner cannot reach.
Rules built into the walls
Every office tower has fire doors. Nobody re-approves them each morning, and the company renting the top floor cannot rip one out because it spoils their open-plan layout. The rule lives in the building, enforced by the city, sitting above any single tenant. Organization policy is Google Cloud's version of that.
Your cloud is shaped like a family tree. The organization sits at the root and stands in for your whole company. Under it sit folders, usually one per team or per environment (a prod folder, a nonprod folder). Under folders sit projects, the actual boxes where your VMs, storage buckets and databases live. Switch a rule on near the top and everything underneath inherits it, the way a building code applies to every floor whether the tenant likes it or not. Two words are worth keeping straight. A constraint is the rule itself ('no external IPs on VMs'). A policy is you turning that rule on at a chosen node of the tree.
Three guardrails every project should be born with
Constraints come in two shapes. A boolean constraint is a light switch: on or off. iam.disableServiceAccountKeyCreation is that kind, and flipping it on means nobody anywhere below it can create a downloadable key. A list constraint is a guest list, where you name the values that are allowed or denied. gcp.resourceLocations is a guest list of regions, so you can say 'EU only' and watch a bucket in Iowa get refused. The third guardrail, compute.vmExternalIpAccess, denies public IP addresses on VMs. Set all three at the org root and every project inherits them the moment it exists, including the one a tired engineer spins up at 2am for a demo.
# sa-keys.yaml: a boolean constraint, enforced at the org root.name: organizations/123456789012/policies/iam.disableServiceAccountKeyCreationspec:rules:- enforce: truegcloud org-policies set-policy sa-keys.yaml# ---> stdout: the resolved policy is echoed backname: organizations/123456789012/policies/iam.disableServiceAccountKeyCreationspec:etag: CO2mxdkGEIC0m4wGrules:- enforce: trueupdateTime: '2026-07-16T10:22:04Z'# Prove a project three levels down inherited it (it has no policy of its own).gcloud org-policies describe iam.disableServiceAccountKeyCreation \--project=team-prod --effective# ---> stdoutname: projects/team-prod/policies/iam.disableServiceAccountKeyCreationspec:rules:- enforce: true
# locations.yaml: a list constraint. Only EU locations are allowed.name: organizations/123456789012/policies/gcp.resourceLocationsspec:rules:- values:allowedValues:- in:eu-locationsgcloud org-policies set-policy locations.yaml# ---> stdoutname: organizations/123456789012/policies/gcp.resourceLocationsspec:etag: CIfa1dkGEIC0m4wGrules:- values:allowedValues:- in:eu-locationsupdateTime: '2026-07-16T10:23:31Z'
Banning public IPs comes with a catch you have to plan for. A VM with no external address still needs to reach out sometimes, to pull operating-system patches or to call a Google API (one of Google's own services, such as its storage or logging endpoints). Take the public address away and the whole outbound path goes with it. So you give the subnet a Cloud NAT (Network Address Translation), a shared one-way exit to the internet that no outside host can dial back into. Ban the IPs and stand up the NAT in the same change, or you strand every private VM you own.
# no-ext-ip.yaml: deny external IPs on every VM in the org.name: organizations/123456789012/policies/compute.vmExternalIpAccessspec:rules:- denyAll: truegcloud org-policies set-policy no-ext-ip.yaml# ---> stdoutname: organizations/123456789012/policies/compute.vmExternalIpAccessspec:rules:- denyAll: trueupdateTime: '2026-07-16T10:24:09Z'# Private VMs still need outbound internet (patching, Google APIs). Give the# subnet a Cloud NAT so egress works without a single public IP on a VM.gcloud compute routers create nat-rtr-euw1 \--network=shared-vpc --region=europe-west1gcloud compute routers nats create euw1-nat \--router=nat-rtr-euw1 --region=europe-west1 \--auto-allocate-nat-external-ips --nat-all-subnet-ip-ranges# ---> stdoutCreated [https://www.googleapis.com/compute/v1/projects/net-hub/regions/europe-west1/routers/nat-rtr-euw1].Creating NAT [euw1-nat] in router [nat-rtr-euw1]...done.
Do not flip a fresh constraint straight to enforced across the whole org and hope for the best. Org policy has a dry-run mode. The policy evaluates, writes a would-have-denied entry into the audit log, and blocks nothing at all. Leave it in dry run for a week and read the log for the surprises: a global load balancer's health-check resources, a Cloud Functions staging bucket that quietly landed in us-central1, a partner's VM that genuinely does need an external address. Fix those or carve exceptions, then enforce for real. Location constraints have a wider blast radius than people expect, because plenty of resources that look 'global' still pin themselves to a physical region underneath.
Here is the payoff. With the policies live, a violating action does not get logged quietly for someone to chase down next week. It fails on the spot, at create time, in the caller's own terminal.
# A developer who holds roles/owner on the project tries to mint a key anyway.gcloud iam service-accounts keys create key.json \# ---> stderrERROR: (gcloud.iam.service-accounts.keys.create) FAILED_PRECONDITION:Key creation is not allowed on this service account.# Same refusal for a VM that asks for a public IP.gcloud compute instances create web-01 \--zone=europe-west1-b --subnet=prod-euw1# ---> stderrERROR: (gcloud.compute.instances.create) Could not fetch resource:- Constraint constraints/compute.vmExternalIpAccess violated for projectteam-prod. Add instance projects/team-prod/zones/europe-west1-b/instances/web-01 to the constraint to use external IP access.
Why even an Owner can't step over them
roles/owner sounds like god mode, and inside one project it very nearly is. What it does not carry is roles/orgpolicy.policyAdmin, the one role that lets you write or change an org policy. That role gets granted up at the organization or folder level, normally to a small platform team. So the Owner can create resources all day and still cannot rewrite the rule that forbids public IPs, because the rule does not live in their project. It sits above them, on a node where they hold no admin rights at all.
When a service checks a constraint it walks from the resource upward toward the org, merging whatever policies it meets on the way. Running describe with --effective prints the answer it lands on. For a boolean constraint, the most specific rule wins. For a list constraint, a child node replaces its parent's values unless you set inheritFromParent: true, and reset: true drops a node back to the constraint's built-in default. That is how you carve one clean exception, say a sandbox folder allowed to have external IPs, without loosening anything else in the org. For rules Google does not ship out of the box, custom constraints let you write your own in a small expression language against a resource's fields, like insisting on Shielded Nodes (a tamper-resistant VM setup) for every GKE cluster (Google Kubernetes Engine, Google's managed service for running containerised apps).
Born fenced: the landing zone
Setting these by hand on day one is easy. Day two hundred is where it falls apart, when someone stands up a project in a hurry and forgets every last one of them. A landing zone takes the choice away. A new hire does not get a blank laptop and a wiki page titled 'please turn on security'. They get a machine IT has already imaged, disk encryption on, the right agent installed. A landing zone does the same thing for cloud projects. A project factory (Terraform's project-factory modules, or Google's Cloud Foundation Fabric FAST) hands out each new project with the baseline already wired in: org policies inherited from the right folder, an aggregated log sink routing every audit log into one central logging project, a Shared VPC attachment (Virtual Private Cloud, a private network shared out from a central project) so networking stays centrally controlled, Security Command Center (Google's built-in threat and misconfiguration scanner) switched on, and a tightly scoped service account in place of the wide-open default editor. Teams file a request and get back a project that was never, for a single second, wide open. For regulated or sovereign work, Assured Workloads goes further and pins the region, the support staff allowed to touch it, and the encryption-key rules to a compliance regime automatically.
A guardrail that is only a suggestion is not a guardrail. An org policy with enforce: true at the org or folder is not a style guide someone can argue with in review; it is a refusal at the API. Start with the handful of constraints that close the most common breach paths: disable service-account key creation, restrict external IPs, require OS Login (so people sign in with their Google identity rather than SSH keys copied between laptops), enforce uniform bucket-level access, and block public members on storage if that fits how you work.
Exceptions belong on a child node, with an expiry date and a named owner. They do not belong as a permanent allValues=ALLOW at the top of the tree, which quietly switches the whole thing off. And the landing zone should stamp the same policy bundle at the moment a project is minted, so nobody ever inherits a naked project along with a promise to harden it next sprint.
Try this
List the org policies on a folder, confirm that disableServiceAccountKeyCreation and one compute constraint both read as enforced, then, in a lab project only, attempt a forbidden action and capture the denial that comes back.
gcloud org-policies list --folder=FOLDER_ID --format="table(constraint,spec.rules[0].enforce)"gcloud org-policies describe constraints/iam.disableServiceAccountKeyCreation --folder=FOLDER_ID# lab only — expect failure:gcloud iam service-accounts keys create /tmp/should-fail.json \
CONSTRAINT ENFORCEconstraints/iam.disableServiceAccountKeyCreation Trueconstraints/compute.requireOsLogin Trueconstraints/compute.vmExternalIpAccess Trueconstraint: constraints/iam.disableServiceAccountKeyCreationspec:rules:- enforce: trueERROR: (gcloud.iam.service-accounts.keys.create) FAILED_PRECONDITION:Key creation is not allowed on this service account.Constraint constraints/iam.disableServiceAccountKeyCreation enforced.
Takeaway
Org policies are the fire doors of your cloud. Put the key-creation ban, the external-IP ban and uniform bucket access on the folder or the org root, where a project Owner holds no role that lets them touch the rule, and have the landing zone stamp the same bundle into every project it hands out.
Next you rehearse what happens when something slips past anyway. Incident response, in order: seal, photograph, re-key, so your detection and your guardrails have a practised last mile.