CoursesGCP securityOrg policy & landing zones

Org policy & landing zones

Preventive constraints and secure-by-default projects.

Advanced30 min · lesson 14 of 15

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.

ban downloadable SA keys, org-wide
# sa-keys.yaml: a boolean constraint, enforced at the org root.
name: organizations/123456789012/policies/iam.disableServiceAccountKeyCreation
spec:
rules:
- enforce: true
gcloud org-policies set-policy sa-keys.yaml
# ---> stdout: the resolved policy is echoed back
name: organizations/123456789012/policies/iam.disableServiceAccountKeyCreation
spec:
etag: CO2mxdkGEIC0m4wG
rules:
- enforce: true
updateTime: '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
# ---> stdout
name: projects/team-prod/policies/iam.disableServiceAccountKeyCreation
spec:
rules:
- enforce: true
pin every resource to EU regions
# locations.yaml: a list constraint. Only EU locations are allowed.
name: organizations/123456789012/policies/gcp.resourceLocations
spec:
rules:
- values:
allowedValues:
- in:eu-locations
gcloud org-policies set-policy locations.yaml
# ---> stdout
name: organizations/123456789012/policies/gcp.resourceLocations
spec:
etag: CIfa1dkGEIC0m4wG
rules:
- values:
allowedValues:
- in:eu-locations
updateTime: '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 public IPs, but keep egress alive
# no-ext-ip.yaml: deny external IPs on every VM in the org.
name: organizations/123456789012/policies/compute.vmExternalIpAccess
spec:
rules:
- denyAll: true
gcloud org-policies set-policy no-ext-ip.yaml
# ---> stdout
name: organizations/123456789012/policies/compute.vmExternalIpAccess
spec:
rules:
- denyAll: true
updateTime: '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-west1
gcloud compute routers nats create euw1-nat \
--router=nat-rtr-euw1 --region=europe-west1 \
--auto-allocate-nat-external-ips --nat-all-subnet-ip-ranges
# ---> stdout
Created [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.

the guardrail refuses a violating create
# A developer who holds roles/owner on the project tries to mint a key anyway.
gcloud iam service-accounts keys create key.json \
# ---> stderr
ERROR: (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
# ---> stderr
ERROR: (gcloud.compute.instances.create) Could not fetch resource:
- Constraint constraints/compute.vmExternalIpAccess violated for project
team-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.

Guardrails inherited down a landing zone
Organization root
Org policies
no SA keys, no public IPs, EU-only locations
Aggregated log sink
every audit log routed to the logging project
Folders (per environment)
prod folder
inherits the org policy, adds stricter overrides
sandbox folder
reset:true carves one exception, e.g. external IP
Vended projects
project factory
Terraform / Fabric FAST stamps the baseline
app project
Shared VPC, SCC on, least-priv SA, born fenced
A rule set once at the root reaches every project below it, and the factory makes sure no project is ever born outside the tree.
Enforcing a constraint doesn't touch what already exists
Switching on compute.vmExternalIpAccess will not strip the public IP off a VM that is already running, so nothing looks broken at first. The break arrives later. The next time a managed instance group (a self-healing pool of identical VMs) replaces one, or a deploy recreates that VM, the create is refused and the rollout wedges with no obvious cause. And any private VM that lost its route to the internet stays broken until a Cloud NAT exists. Roll constraints out in dry run first, and stand up the NAT before you enforce, not after the pager goes off.

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.

terminal
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 \
output
CONSTRAINT ENFORCE
constraints/iam.disableServiceAccountKeyCreation True
constraints/compute.requireOsLogin True
constraints/compute.vmExternalIpAccess True
constraint: constraints/iam.disableServiceAccountKeyCreation
spec:
rules:
- enforce: true
ERROR: (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.

Quick check
01You enforce constraints/iam.disableServiceAccountKeyCreation at the organization. A developer with roles/owner on their project can no longer create a key, and says they will override the policy on their own project instead. What actually stops them?
Correct — Writing or relaxing an org policy needs a separate role granted at the org or folder, and the project Owner role does not carry it. That is exactly why the guardrail holds.
Incorrect — No. An Owner can build resources freely, but changing an org policy needs orgpolicy.policyAdmin, which roles/owner does not grant.
Incorrect — No. A policy admin can change or reset a policy any time. What protects it is who holds that role, and a project Owner does not.
Incorrect — No. The constraint refuses the violating call at create time. It never deletes a project.
02A gcp.resourceLocations org policy at the organization root allows EU locations only. A platform admin then sets a policy on the sandbox folder that allows a US region, and does not set inheritFromParent. For projects inside that sandbox folder, which locations end up allowed?
Incorrect — No. Merging is what inheritFromParent: true switches on, and it was left off here.
Correct — For a list constraint the more specific node overrides, so the sandbox folder's US value stands on its own.
Incorrect — No. The root does not automatically win. The most specific node governs, which here is the sandbox folder.
Incorrect — No. Nothing cancels. The child's value replaces the parent's for that node and resolution returns one answer.
03On a Friday you enforce compute.vmExternalIpAccess (deny external IPs) at the org root. Everything keeps running all weekend. On Tuesday a managed instance group tries to replace an unhealthy VM and the rollout jams. Why did nothing break until then?
Incorrect — No. Org policy applies to new actions straight away. There is no multi-day propagation delay.
Incorrect — No. Org policy does not strip IAM roles. It blocks the specific create that violates the constraint.
Incorrect — No. You enforced directly rather than in dry run, so there was no grace period counting down.
Correct — The running VMs kept their IPs, and the refusal waited until something asked to create a new one.

Related