Chapter 2

Accounts, Organizations, and Resource Hierarchy

Scope. This chapter builds the foundation everything else sits on: the identity account that owns the organization, the billing account that funds it, the folder and project structure that carries policy, and the organization policy baseline that makes the structure enforceable. It covers naming, labeling, tagging, the project lifecycle, and the two reference hierarchies used throughout the book. IAM roles and bindings are defined here only where the hierarchy requires them; the IAM model itself is Chapter 3, and networking placement is Chapter 5. Prerequisites. Chapter 1, particularly §1.5–1.7 (project as boundary, hierarchy, inheritance) and §1.12 (secure vs. permissive defaults). Verified against. Google Cloud console and API surface as of 2026-09, Cloud SDK 583.0.0, hashicorp/google provider 8.x; see sources at end.

The hierarchy is the only control in Google Cloud that you cannot retrofit cheaply. IAM bindings can be rewritten in an afternoon, firewall policies in a morning, but moving a production workload out of a project it should never have shared means new project IDs, new service account emails, new IAM references in a dozen other projects, and a migration window. Every organization that has run GCP for more than two years has a folder somewhere named after a team that no longer exists, holding projects nobody will move.

So this chapter is written as a set of decisions to make once, deliberately, with their consequences stated. Three of them dominate. The first is what a project means in your organization — one system per project, one environment per project, and never both a production and a non-production workload in the same one. The second is what the folder layer is for: policy and delegation, not org-chart mirroring, because org charts change annually and folders do not want to. The third is which guardrails are enforced at the organization node from day one, because a constraint added after five hundred projects exist is a migration project, whereas the same constraint added on day one is invisible.

The chapter also fixes concrete conventions — project ID format, mandatory labels, tag keys, region allowlists — that every later chapter's examples use. Those conventions are not aesthetic. A project ID that encodes environment lets an IAM condition, a log filter, an org policy, and a budget alert all key on the same string without a lookup table.

2.1 Google Accounts §

Every principal that authenticates interactively to Google Cloud is a Google account: an email identity with credentials, MFA settings, and a session. There are three kinds you will encounter, and conflating them causes durable problems.

  • Managed accounts — accounts in a domain you control through Cloud Identity or Google Workspace (alice@rickcollette.domain). You can enforce MFA, suspend them, and recover them. These are the only accounts that belong in a production IAM policy.
  • Consumer accounts@gmail.com accounts, or accounts on a domain you have not verified. You cannot enforce policy on them, cannot suspend them, and cannot audit their session controls. If one holds a role in your organization, that role survives the person leaving.
  • Unmanaged accounts — consumer-style accounts created with an email address in your domain before you claimed it in Cloud Identity. These are the dangerous ones: they look managed and are not. Cloud Identity's transfer tool reclaims them, and doing so is a prerequisite for a trustworthy org.

Security posture. The control that closes this off is domain-restricted sharing, constraints/iam.allowedPolicyMemberDomains, which refuses IAM bindings for principals outside the allowed customer IDs. On organizations created on or after 2024-05-03 it is enforced by default with your own domain as the sole allowed value. On an older organization, set it explicitly before anything else (§2.30).

Pitfall. Domain-restricted sharing takes customer IDs, not domain strings, and it blocks legitimate Google-managed principals in some flows (for example, some marketplace and support integrations). Roll it out dry-run first and read the denied entries before enforcing.

2.2 Google Workspace and Cloud Identity §

Google Cloud has no user database of its own. Users, groups, and their credentials live in Cloud Identity (the free or premium identity-only product) or Google Workspace (identity plus the productivity applications). Either one provides the same GCP-relevant surface: user lifecycle, groups, SSO from an external IdP, MFA enforcement, and the domain verification that makes an organization resource possible.

The mapping is strict: one Cloud Identity or Workspace account is associated with exactly one Google Cloud organization. You cannot split one identity tenant across two organizations, and you cannot point one organization at two identity tenants.

For a SecDevOps practice, the properties that matter are:

  • Groups are the unit of IAM grant. Bind roles to group: principals, never to individual users. Group membership is managed in Cloud Identity, is auditable there, and changes without touching a single IAM policy.
  • SSO and provisioning belong upstream. Federate authentication to the corporate IdP and provision users with SCIM or Google Cloud Directory Sync so that the leaver process in HR actually removes GCP access (§4.4).
  • Super administrators are not GCP roles. A Cloud Identity super admin can reset any password and re-grant any role. Keep the count in single digits, require hardware security keys, exclude them from day-to-day work, and monitor their use as a break-glass path.

Console path. Google Admin console (admin.google.com) → Directory → Users / Groups, and Security → Authentication for MFA and SSO. This is a different console from console.cloud.google.com; the split trips up new administrators.

2.3 Creating a GCP Organization §

The organization resource is not created by an API call you make. It is provisioned automatically once your domain is associated with Cloud Identity or Workspace: for a brand-new tenant, when a user first signs in to the Cloud console and accepts the terms; for an existing tenant, when a user first creates a project or a billing account.

The practical sequence:

  1. Sign up for Cloud Identity (free edition is sufficient) and verify DNS ownership of the domain.
  2. Reclaim any unmanaged accounts in the domain (§2.1).
  3. Have a user in the domain sign in to the Cloud console; the organization appears.
  4. Note the numeric organization ID — it is the anchor for every org-level policy you will write.
gcloud organizations list --format="table(displayName,name,owner.directoryCustomerId)"
gcloud organizations describe 123456789012

Security posture. At creation, every user in the domain receives roles/resourcemanager.projectCreator and roles/billing.creator at the organization node. That is deliberate on Google's part — it preserves service for people who were already using projects — and it is the first thing to remove. Until you do, any employee can create a project outside your hierarchy conventions and attach it to a billing account.

IAM/API. Resource Manager v3, organizations.get and organizations.setIamPolicy; permissions resourcemanager.organizations.get, resourcemanager.organizations.setIamPolicy.

2.4 Organization Administrators §

Two distinct privileged identities govern the top of the hierarchy, and separating them is the first separation-of-duties control in the book.

RoleWhere it livesWhat it can doWho should hold it
Super AdministratorCloud Identity / WorkspaceReset any password, alter MFA, claim any org, recover accounts2–4 people, break-glass only, hardware keys
Organization Administrator (roles/resourcemanager.organizationAdmin)Google Cloud IAMSet allow and deny policies at the org, grant roles, manage the hierarchy's policyA small platform-security group

Organization Administrator deliberately does not include permission to create folders or projects. That is roles/resourcemanager.folderCreator and roles/resourcemanager.projectCreator, granted separately — usually only to the automation identity that runs the project factory (§2.26).

gcloud organizations add-iam-policy-binding 123456789012 \
  --member="group:gcp-org-admins@rickcollette.domain" \
  --role="roles/resourcemanager.organizationAdmin"

Anti-pattern. Leaving super administrators with Organization Administrator as well. It collapses the two control planes into one identity, and it means the account that can reset MFA can also delete the audit sink.

Pitfall. Removing the default org-wide projectCreator grant without first granting it to your factory identity locks everyone out of creating projects, including your pipeline. Grant, then remove, then verify.

2.5 Billing Accounts §

A Cloud Billing account is the payment instrument. A project links to exactly one billing account, and a billing account can fund many projects. Crucially, a billing account is not an IAM parent of a project: nothing in the project inherits permissions from the billing account, and nothing in the billing account inherits from the organization's project hierarchy.

The billing IAM roles, with their exact identifiers:

RoleIDGrants
Billing Account Creatorroles/billing.creatorCreate new self-serve billing accounts (org-level)
Billing Account Administratorroles/billing.adminManage a billing account, payment methods, links, and its IAM
Billing Account Userroles/billing.userAttach projects to that billing account
Project Billing Managerroles/billing.projectManagerLink/unlink a project, without resource access
Billing Account Costs Managerroles/billing.costsManagerManage budgets and BigQuery cost export
Billing Account Viewerroles/billing.viewerRead cost and payment data

Least-privilege pattern. Finance gets roles/billing.viewer plus roles/billing.costsManager. The project factory identity gets roles/billing.user on the billing account and roles/billing.projectManager on the folder, which together let it create a project and attach it — and nothing else. roles/billing.creator is removed from the domain-wide default grant (§2.3) and given to nobody by standing grant.

gcloud billing accounts list --format="table(name,displayName,open)"
gcloud billing projects link rc-saas-prod-app-01 \
  --billing-account=01A2B3-C4D5E6-F7G8H9

Security posture. Billing is a denial-of-service surface: closing a billing account stops every project it funds. Budget alerts are also your cheapest compromise detector (§1.24). Route them to security as well as finance, with a Pub/Sub topic rather than only email so automation can act.

2.6 Resource Manager §

Cloud Resource Manager (cloudresourcemanager.googleapis.com) is the API that owns the hierarchy itself: organizations, folders, projects, tag keys, tag values, and tag bindings. Every structural operation in this chapter is a Resource Manager call.

The v3 API surface you will actually use:

ResourceMethodsKey permissions
organizationsget, search, getIamPolicy, setIamPolicyresourcemanager.organizations.*
folderscreate, patch, move, delete, undelete, list, search, *IamPolicyresourcemanager.folders.create, .move, .delete
projectscreate, get, patch, move, delete, undelete, search, *IamPolicyresourcemanager.projects.create, .delete, .setIamPolicy
tagKeys / tagValues / tagBindingscreate, list, deleteresourcemanager.tagKeys.create, resourcemanager.tagValues.use

Resource Manager is also what Cloud Asset Inventory indexes, which is how you query the hierarchy at scale:

gcloud asset search-all-resources \
  --scope="organizations/123456789012" \
  --asset-types="cloudresourcemanager.googleapis.com/Project" \
  --query="labels.env:prod" \
  --format="table(name,displayName,labels)"

Pitfall. Resource Manager mutations are eventually consistent and rate-limited at low per-second values. A project factory that creates fifty projects in a loop will hit those limits; build in retry with backoff rather than raising the parallelism.

2.7 Organizations §

The organization is the root node and the only place where certain controls can be applied at all: aggregated log sinks that capture every project, Security Command Center at organization scope, hierarchical firewall policies covering the whole estate, Access Transparency, and organization policies that no project can escape.

Design rules for the org node:

  • Grant almost nothing here. Org-level bindings apply to every resource that exists or ever will exist under it. Reserve the org node for: Organization Administrator, Security Command Center admin, org policy admin, log viewer for the security team, and Billing Account User for the factory.
  • Apply the policy baseline here (§2.30), so a newly created folder inherits it before anyone can put a project in it.
  • Put the aggregated log sink here, with --include-children, so no project can be created outside audit coverage (§1.20).
gcloud logging sinks create org-audit-sink \
  logging.googleapis.com/projects/rc-saas-shared-log-01/locations/global/buckets/org-audit \
  --organization=123456789012 \
  --include-children \
  --log-filter='logName:"cloudaudit.googleapis.com"'

Pitfall. An organization cannot be deleted while it holds projects, and it cannot be merged with another. The org you create on day one is permanent. Verify the domain you want before the first sign-in creates it.

2.8 Folders §

A folder is a policy and delegation node. It holds IAM bindings, organization policies, tag bindings, and hierarchical firewall policies, and it exists so you can apply those to a group of projects at once.

Structural limits (observed 2026-09): folders nest up to 10 levels deep, and a parent holds at most 300 direct child folders.

What folders should represent. In order of usefulness:

  1. Environment — the most valuable folder boundary, because production policy genuinely differs from development policy and the difference must be enforced, not documented.
  2. Trust or compliance zone — PCI scope, a regulated data domain, an air-gapped workload.
  3. Business unit or tenant — only when the unit genuinely administers its own projects (§2.34).

What folders should not represent. Teams, cost centers, or products. Those change too often and are better expressed as labels (§2.12) and tags (§2.13), both of which can be changed without moving a resource.

gcloud resource-manager folders create \
  --display-name="production" \
  --folder=FOLDER_ID_ENVIRONMENTS
gcloud resource-manager folders list --organization=123456789012 \
  --format="table(displayName,name,parent)"
resource "google_folder" "environments" {
  display_name        = "environments"
  parent              = "organizations/123456789012"
  deletion_protection = true
}

resource "google_folder" "production" {
  display_name        = "production"
  parent              = google_folder.environments.name
  deletion_protection = true
}

deletion_protection defaults to true in provider 8.x, so a terraform destroy that would remove a folder fails until you deliberately set it to false. Leave it on for every folder above the workload layer.

Pitfall. Folder display names are not unique and are not identifiers. Automation must resolve and store the numeric folder ID; a script that looks up "production" by display name will one day find two.

2.9 Projects §

The project is where resources actually live (§1.5). Its three identifiers, their constraints, and the ones automation must use:

FieldConstraintMutable
Project ID6–30 chars, lowercase letters/digits/hyphens, starts with a letter, does not end with a hyphen, globally unique, never reusableno
Project numberassigned by Googleno
Display namehuman-readableyes

Restricted substrings including google, ssl, undefined, and null are rejected in project IDs.

gcloud projects create rc-saas-prod-app-01 \
  --name="SaaS Prod App" \
  --folder=FOLDER_ID_PRODUCTION \
  --labels=env=prod,system=app,cost-center=cc-1042,data-class=confidential \
  --no-enable-cloud-apis

--no-enable-cloud-apis suppresses the default set of API enablements so that service enablement becomes an explicit, reviewable act rather than a side effect of creation.

resource "google_project" "prod_app" {
  name            = "SaaS Prod App"
  project_id      = "rc-saas-prod-app-01"
  folder_id       = google_folder.production.folder_id
  billing_account = var.billing_account_id
  deletion_policy = "PREVENT"

  labels = {
    env         = "prod"
    system      = "app"
    owner       = "platform-team"
    cost-center = "cc-1042"
    data-class  = "confidential"
  }
}

resource "google_project_service" "prod_app" {
  for_each = toset([
    "compute.googleapis.com",
    "logging.googleapis.com",
    "monitoring.googleapis.com",
  ])
  project                    = google_project.prod_app.project_id
  service                    = each.value
  disable_dependent_services = false
  disable_on_destroy         = false
}

Set deletion_policy = "PREVENT" on every production project. The provider will then refuse to destroy it, which is the correct behavior for a resource whose deletion takes a support case to undo.

API/IAM. projects.create on cloudresourcemanager.googleapis.com; permission resourcemanager.projects.create, in roles/resourcemanager.projectCreator. Note that the provider reads the billing account on every plan, so the Terraform identity needs roles/billing.user on it.

Pitfall. Do not set auto_create_network = false on google_project as your control against the default network. Google's own provider documentation recommends the constraints/compute.skipDefaultNetworkCreation organization policy instead, because the org policy also covers projects created outside Terraform.

2.10 Naming Standards §

A naming standard exists so that a machine can parse identity, environment, and ownership out of a name without a lookup. Every convention below is used by later chapters' IAM conditions, log filters, and org policies.

The rule: <prefix>-<env>-<function>-<nn>

  • prefix — the organization or tenant short name: rc-saas, rc-ent.
  • env — one of bootstrap, shared, dev, stg, prod, sbx. Fixed vocabulary, no synonyms.
  • function — the system or platform role: app, data, net, log, sec, cicd.
  • nn — a two-digit ordinal for horizontal growth.

Applied consistently: rc-saas-prod-app-01, rc-saas-shared-net-01, rc-ent-dev-data-02.

Names for other resource classes derive from the project:

ResourcePatternExample
Folderlowercase, single word or dashedproduction
Service accountsa-<function>-<purpose>sa-app-api-runtime
VPC networkvpc-<env>-<region-scope>vpc-prod-global
Subnetsnet-<env>-<region>-<tier>snet-prod-usc1-app
Bucket<project-id>-<purpose>rc-saas-prod-app-01-uploads
Custom rolerc.<domain><Action>rc.deployReleaser

Why the nn matters. Names are immutable for projects, buckets, and service accounts. When a system needs a replacement — a re-platform, a region migration — the ordinal is what lets you stand up -02 alongside -01 and cut over. Without it, you get rc-saas-prod-app-new, and then -new2.

Pitfall. Bucket names are in a single global namespace shared with every other Google Cloud customer. Prefixing with the project ID both guarantees uniqueness and prevents the enumeration that generic names invite.

2.11 Project ID Conventions §

Project IDs deserve their own rules because they are the most constrained and least changeable identifier in the platform.

  • 30 characters is the hard ceiling. rc-saas-prod-app-01 is 21; a four-part name with a long prefix runs out fast. Reserve at least six characters of headroom for the function segment.
  • Never encode a team, a person, or a date. They change; the ID cannot.
  • Never reuse. Deleted project IDs are permanently retired, so a rebuild must increment the ordinal.
  • Make the environment segment machine-parsable, because IAM conditions and org policies key on it:
resource.name.startsWith("projects/rc-saas-prod-")
  • Keep the display name separate and human. SaaS Production — Application Platform is fine as a display name and impossible as an ID.

Anti-pattern. Random or hash-suffixed project IDs (rc-saas-app-8fj2k1). They guarantee uniqueness and destroy every condition, filter, and dashboard that would otherwise have keyed on the name. If you need collision resistance in a self-service factory, put the randomness in the ordinal position and record the mapping in the factory's inventory.

2.12 Labels §

Labels are key-value metadata attached directly to a resource. They are not inherited, they are not usable in IAM conditions or organization policy, and they exist primarily for cost attribution, inventory, and filtering.

Limits (observed 2026-09): up to 64 labels per resource; keys 1–63 characters and values 0–63 characters; lowercase letters, digits, underscores, and dashes only; keys must start with a lowercase letter or an international character; keys unique within a resource.

The mandatory label set for every project and every significant resource in this book:

KeyValuesPurpose
envbootstrap,shared,dev,stg,prod,sbxenvironment filtering, cost split
ownerteam slugincident routing
cost-centercc-NNNNchargeback
data-classpublic,internal,confidential,restricteddrives control expectations
systemsystem slugblast-radius mapping

Labels are applied at creation with gcloud projects create --labels=.... Afterward, gcloud projects update changes only the display name (--name), so updating labels on an existing project means the Resource Manager projects.patch method, Terraform, or Console → IAM & Admin → Labels:

resource "google_project" "prod_app" {
  # ...
  labels = {
    env         = "prod"
    system      = "app"
    owner       = "platform-team"
    cost-center = "cc-1042"
    data-class  = "restricted"
  }
}

Security posture. data-class is the label that earns its place: it lets a detection rule say "alert on any public bucket in a project labeled restricted" and lets a report answer "which regulated systems lack CMEK." Enforce its presence with a custom organization policy constraint (§2.30), because a label nobody sets is worse than no label — it makes the gap invisible.

Pitfall. Because labels are not inherited, applying env=prod to a folder does nothing for the projects inside it. That inheritance is what tags are for.

2.13 Tags §

Tags are the inheritable, policy-capable metadata system. A TagKey and a TagValue are discrete resources created at the organization or project level; a TagBinding attaches a value to a target resource. Descendants inherit the binding unless they bind a different value of the same key, and the resulting set is called the resource's effective tags.

Limits (observed 2026-09): 50 key-value pairs per resource, 1,000 keys per organization, 1,000 values per key; tag strings up to 256 characters versus 63 for labels.

The difference that matters:

TagsLabels
Discrete resources with their own IAMyesno
Inherited by descendantsyesno
Usable in IAM conditionsyesno
Usable in organization policy conditionsyesno
Must be defined before useyesno
gcloud resource-manager tags keys create environment \
  --parent=organizations/123456789012 \
  --description="Deployment environment; drives conditional IAM and org policy."

gcloud resource-manager tags values create production \
  --parent=123456789012/environment \
  --description="Production workloads."

gcloud resource-manager tags bindings create \
  --tag-value=123456789012/environment/production \
  --parent=//cloudresourcemanager.googleapis.com/projects/123456789012
resource "google_tags_tag_key" "environment" {
  parent      = "organizations/123456789012"
  short_name  = "environment"
  description = "Deployment environment."
}

resource "google_tags_tag_value" "production" {
  parent      = google_tags_tag_key.environment.id
  short_name  = "production"
  description = "Production workloads."
}

resource "google_tags_tag_binding" "prod_app" {
  parent    = "//cloudresourcemanager.googleapis.com/projects/${google_project.prod_app.number}"
  tag_value = google_tags_tag_value.production.id
}

Security posture. Tags carry their own IAM: roles/resourcemanager.tagUser is needed to attach a value, separately from permission on the target resource. That separation is the point — it lets you say "developers may create resources here, but only the platform team may mark something as production," and then condition access on the tag.

Pitfall. google_tags_tag_binding binds to the project number, not the project ID, and the tags argument on google_project and google_folder is set only at creation — changing it forces resource replacement. For an existing resource, use a tag binding.

2.14 Resource Metadata §

Beyond labels and tags, three metadata surfaces carry security weight.

  • Essential Contacts — per-category notification addresses (Security, Technical, Billing, Legal, Suspension) set at the organization, folder, or project and inherited downward. This is the channel Google uses for security notices and account suspension warnings; an unset Security contact means a compromise notice reaches nobody. Constrain the allowed domains with constraints/essentialcontacts.managed.allowedContactDomains, which is enforced by default on organizations created on or after 2024-05-03.
  • Compute Engine instance and project metadata — key-value data served to VMs by the metadata server. It is also where SSH keys land if you let them, and where startup scripts live. Treat project-level metadata as a privileged surface: anything you put there is readable by every VM in the project, and enable-oslogin=TRUE in project metadata is the counterpart to the OS Login organization policy.
  • Cloud Asset Inventory metadata — the searchable index of every resource, its IAM policy, its org policies, and its ancestry, with five weeks of history. It is the only practical way to answer hierarchy-wide questions.
gcloud asset search-all-resources \
  --scope="organizations/123456789012" \
  --query="NOT labels.data-class:*" \
  --asset-types="cloudresourcemanager.googleapis.com/Project" \
  --format="value(name)"

Pitfall. Never put secrets in instance or project metadata. It has no encryption boundary of its own, is readable by anything on the instance including a compromised process, and appears in describe output for anyone with compute.projects.get. Secret Manager exists for this.

2.15 Environment Separation §

Environment separation is a project boundary, not a naming convention. The rule this book applies without exception:

A production workload and a non-production workload never share a project.

The reasons are mechanical, not philosophical. A project shares quota, so a load test in staging can exhaust production's API quota. It shares the default IAM boundary, so a project-level grant to a developer reaches production data. It shares service enablement and default service accounts. It shares a VPC unless you deliberately do otherwise.

Separation extends upward into folders so that policy can differ:

LayerDevelopmentProduction
Human write accessstanding, group-basedjust-in-time only (§3.27)
Deploy identitydev pipeline SAprod pipeline SA, separate WIF pool
External IPspermitted for conveniencedenied by org policy
Datasynthetic onlyreal, CMEK-encrypted
Deletion protectionoffon
Data Access logsoffon for data services

Anti-pattern. A single "shared services" project that non-production and production both depend on. When it breaks, everything breaks; when it is compromised, everything is compromised. Where a genuinely shared dependency exists — an artifact registry, a KMS key ring — the dependency is shared read-only and the administration is separate.

2.16 Development Projects §

Development projects optimize for iteration speed, on the explicit condition that they contain no real data.

Posture.

  • Standing broad project-write access is still wrong; grant scoped predefined roles to a developer group at the project.
  • Real customer data is forbidden. Enforce with detection: Sensitive Data Protection discovery scans on development buckets and datasets, alerting on any finding.
  • External IPs may be permitted; SSH is still via IAP TCP forwarding (§4.16), because the habits formed in development are the habits used in production.
  • Budgets are tight and alerting, because development is where a mining compromise lands first.
  • Data Access logging is off, to control cost.

Naming. rc-saas-dev-app-01, in fldr-development, tagged environment=development.

Development projects are also where per-developer sandboxes get confused with a shared dev environment. Keep them separate: a shared dev project is a deployment target with a pipeline, and a sandbox is disposable and personal (§2.25).

2.17 Test Projects §

Test projects host automated testing — integration suites, contract tests, ephemeral environments created and destroyed by CI.

Posture.

  • The pipeline owns the project, not humans. Human access is read-only; the test service account has scoped write access to exactly the resource types the suite creates.
  • Everything is ephemeral. Resources carry a ttl label and a scheduled job deletes anything past it. This is a security control as much as a cost control: a forgotten test VM with a public IP is a durable foothold.
  • Test data is synthetic or masked. If a test genuinely needs production-shaped data, it gets de-identified through Sensitive Data Protection first, and that pipeline is itself audited.
  • Credentials are short-lived. The test identity federates in (§4.7) and impersonates a per-suite service account.

Pitfall. Test projects accumulate IAM grants faster than any other kind, because every failing test produces a "just add the permission" commit. Run IAM Recommender against them on a schedule and treat unused roles as defects (§3.31).

2.18 Staging Projects §

Staging exists to be production-shaped. Its value is entirely proportional to how closely it matches production, which makes it the environment most often quietly degraded.

What must match production:

  • The same organization policies. If production denies external IPs, staging denies them too — otherwise staging never catches the change that needs one.
  • The same network topology: same Shared VPC pattern, same firewall policy structure, same Private Google Access and Cloud NAT configuration.
  • The same identity model: the deploy pipeline uses the same federation mechanism and the same role set, with staging-scoped bindings.
  • The same admission controls: Binary Authorization enforced, the same attestors, the same policy bundle.

What must differ:

  • The data. Staging holds de-identified or synthetic data, always.
  • The KMS keys and the secrets. Never share a key ring or a secret between staging and production; a staging compromise must not decrypt production.
  • The blast radius. Separate projects, separate service accounts, separate WIF pool, separate billing budget.

Anti-pattern. Granting the production deploy service account access to staging "so the promotion works." Promotion is a Cloud Deploy or pipeline concern with its own identity per target; a single identity spanning both environments erases the boundary you built.

2.19 Production Projects §

Production is defined by what is not possible in it.

Baseline posture:

  • No standing human write access. Read access is standing and group-based; write is granted just-in-time through Privileged Access Manager with justification and expiry (§3.27, §3.28).
  • All change through the pipeline, whose identity is federated and scoped per environment (§1.13).
  • Deletion protection on: deletion_policy = "PREVENT" on the project, deletion protection on databases and folders, bucket retention or soft-delete on data.
  • Org policy strict: no external IPs, OS Login required, resource locations restricted, service usage restricted, public access prevention enforced.
  • Full telemetry: Data Access logs on for IAM, KMS, Secret Manager, Cloud Storage, and BigQuery; log sink to the central logging project; Security Command Center findings routed to the SOC.
  • CMEK where the data classification requires it, with keys in a separate project the workload cannot administer (§2.21).
  • Essential Contacts set, security category populated with a monitored distribution list.

Verification. The check that matters is not "is the policy documented" but "does the API refuse the call." For each control in the list, there is a gcloud org-policies describe ... --effective or a get-iam-policy that answers definitively; the Security Checklist at the end of this chapter enumerates them.

2.20 Shared Infrastructure Projects §

Some resources are legitimately shared across environments. They belong in dedicated projects under a common folder, administered by the platform team, and consumed read-only by workloads.

ProjectHoldsConsumed how
rc-saas-shared-net-01Shared VPC host, subnets, Cloud NAT, DNS zones, firewall policiesservice projects attach (§5.29)
rc-saas-shared-art-01Artifact Registry repositoriespull via IAM on the repository
rc-saas-shared-sec-01KMS key rings, Secret Manager, SCC configencrypt/decrypt permission only
rc-saas-shared-log-01Log buckets, BigQuery audit datasetswrite via sink service accounts
rc-saas-shared-cicd-01Cloud Build, Cloud Deploy, WIF poolsimpersonation targets

The governing rule: consumers get the narrowest possible verb. A workload that needs to decrypt gets roles/cloudkms.cryptoKeyEncrypterDecrypter on one key, not on the key ring and never roles/cloudkms.admin. A workload that pulls images gets roles/artifactregistry.reader on one repository.

Pitfall. Shared projects become the single point of failure the whole estate depends on. Give them stricter change control than the workloads they serve, not looser — including deletion protection, org policy, and a separate approval group.

2.21 Security Projects §

The security project holds the controls that must survive the compromise of the things they protect.

Contents: Cloud KMS key rings and keys, Secret Manager secrets that cross project boundaries, Security Command Center configuration, Sensitive Data Protection templates and inspection jobs, and the Terraform state bucket for the bootstrap layer.

Design rules:

  1. No workload runs here. It hosts control resources only, so its attack surface is IAM alone.
  2. Administration is separate from use. The platform-security group administers keys; workloads receive only roles/cloudkms.cryptoKeyEncrypterDecrypter on the specific key version path they need.
  3. Key destruction is delayed and monitored. Cloud KMS schedules key version destruction with a delay — do not shorten it, and alert on DestroyCryptoKeyVersion.
  4. State is treated as a secret store. Terraform state contains generated passwords, keys, and full configuration. Uniform bucket-level access, object versioning, no allUsers, and access limited to the pipeline service account.
gcloud logging read \
  'protoPayload.methodName=("DestroyCryptoKeyVersion" OR "SetIamPolicy")' \
  --project=rc-saas-shared-sec-01 --limit=20 --freshness=7d

Anti-pattern. Keeping keys in the same project as the data they encrypt. CMEK's security value comes from the ability to revoke or destroy the key independently of whoever controls the data — which requires a separate administrative boundary.

2.22 Logging Projects §

The logging project is the evidence store, and its single design requirement is that it must not be modifiable by anyone who can modify the systems it observes.

Structure.

  • An aggregated sink at the organization with --include-children, so projects created later are covered automatically (§2.7).
  • Destinations sized to purpose: a Cloud Logging bucket with a locked retention policy for the operational window, a BigQuery dataset for analysis, and a Cloud Storage bucket with bucket lock for long-term regulatory retention.
  • Separate IAM. The security team gets roles/logging.viewer and BigQuery data viewer; nobody who administers a production project gets write access to the destinations.
  • Exclusions are reviewed changes. A sink exclusion is a hole in the audit trail; require the same review as an IAM change.

Retention reality. Admin Activity and System Event logs are retained 400 days in the originating project at no charge, but Data Access and Policy Denied default to 30 days (§1.20). Anything you must retain longer has to be sunk out.

Pitfall. The sink's writer identity needs permission on the destination; the sink is created first and then granted, so the first minutes of a new sink silently drop entries. Grant the writer identity and verify with a test entry before declaring coverage.

2.23 Networking Projects §

Networking is centralized in a host project per environment, which owns the Shared VPC, its subnets, Cloud NAT, Cloud Router, Cloud DNS zones, and the firewall policies. Workload projects attach as service projects and see the subnets they are granted.

The placement decision is a hierarchy decision and belongs here; the mechanics are §5.27§5.30.

Why centralize:

  • Firewall policy and routing are managed by one team with one review path.
  • Workload teams cannot create a network path that bypasses inspection.
  • IP address planning stays coherent across the estate (§5.6).

IAM split: network administrators get roles/compute.networkAdmin and roles/compute.securityAdmin on the host project; service project teams get roles/compute.networkUser on specific subnets, not on the host project. That subnet-scoped grant is the whole control.

Pitfall. A Shared VPC host project is hard to move and impossible to delete while attachments exist. Place it under the common folder from the start, and constrain which projects may attach with constraints/compute.restrictSharedVpcHostProjects.

2.24 CI/CD Projects §

The CI/CD project holds the build and deploy machinery: Cloud Build triggers and worker pools, Cloud Deploy delivery pipelines, Workload Identity Federation pools and providers for external CI, and the artifact attestors used by Binary Authorization.

This project is the highest-value target in the organization, because its identities can write to production by design. Treat it accordingly:

  • One WIF pool per external system, one provider per repository set, with attribute conditions pinning repository, ref, and (where the provider supports it) the workflow identity — never a wildcard on the subject (§4.7).
  • One deploy service account per environment, impersonated by the pipeline identity rather than granted to it directly, so the impersonation is separately logged and separately revocable.
  • Private worker pools on a VPC with no external IP for anything that touches internal systems.
  • No human write access to triggers or pipeline definitions outside of code review; the trigger configuration is itself Terraform.

Anti-pattern. A single sa-cicd-deployer with a role at the organization node "so it can deploy anywhere." That account becomes equivalent to Organization Administrator, reachable by anyone who can merge a workflow file.

2.25 Sandbox Projects §

A sandbox is a personal, disposable project for learning and experimentation. It is the pressure-release valve that keeps people from experimenting in shared environments.

The bargain: broad permissions inside, hard walls around.

  • Placed under a dedicated sandbox folder with its own, more permissive policy set — but the non-negotiables still apply: domain-restricted sharing, no service account keys, restricted resource locations.
  • A hard budget with an alert at 50% and an automated shutdown path at 100% via Pub/Sub.
  • A TTL. Sandboxes expire, typically 30 days, and are deleted automatically. Notification precedes deletion.
  • No connectivity to shared VPCs, no access to shared KMS keys or artifact repositories holding production images, no real data.
  • Full audit coverage — sandboxes are not exempt from the aggregated log sink.

Pitfall. Sandboxes drift into production use. The TTL and the absence of network connectivity are what prevent it; a sandbox that never expires and can reach the Shared VPC is just an ungoverned production project.

2.26 Project Factory Patterns §

A project factory is the automation that turns a request into a correctly configured project. Manual creation cannot produce consistency at any scale, and consistency is what makes org-wide controls meaningful.

Minimum output of a factory, all in one transaction:

  1. Project created with a generated ID in the correct folder.
  2. Billing account linked.
  3. Mandatory labels applied; environment tag bound.
  4. Baseline APIs enabled (and only those).
  5. Baseline IAM: an owner group with scoped roles, the pipeline service account, no basic roles.
  6. Default service account role grants suppressed; default network absent.
  7. Attached as a Shared VPC service project with the correct subnet grants, if applicable.
  8. Log sink coverage confirmed (inherited from the org sink).
  9. Budget created with alert thresholds.
  10. An entry written to the project inventory.

Implementation shape. A Terraform module invoked from a repository where the request is a small YAML or tfvars file; the pull request is the approval; the pipeline identity holds roles/resourcemanager.projectCreator on the target folder and roles/billing.user on the billing account, and nothing else.

module "project" {
  source = "../../modules/project-factory"

  prefix          = "rc-saas"
  environment     = "prod"
  function        = "app"
  ordinal         = "01"
  folder_id       = google_folder.production.folder_id
  billing_account = var.billing_account_id
  cost_center     = "cc-1042"
  data_class      = "confidential"
  owner_group     = "app-platform@rickcollette.domain"
  apis            = ["compute.googleapis.com", "logging.googleapis.com"]
}

Pitfall. Resource Manager API rate limits are low (fractions of a request per second for some methods). A factory that provisions many projects at once must serialize and retry rather than fan out.

2.27 Project Lifecycle Management §

A project has a lifecycle beyond creation, and most organizations manage only the first step.

StageTriggerRequired action
Requestintake form / PRvalidate naming, owner, cost center, data class
Provisionmergefactory run (§2.26)
Operateongoingdrift detection, IAM recertification, budget review
Reviewquarterlyconfirm owner still exists; confirm labels current
Decommissionowner departure, system retirementinventory resources, export data, revoke IAM, delete
Purge30 days after deletionverify permanent deletion; retain evidence

Ownership is the control that decays. An owner label pointing at a disbanded team means nobody approves the project's IAM recertification and nobody notices its budget. Automate the check: reconcile the owner label against Cloud Identity groups and flag projects whose owning group no longer exists or has no members.

gcloud asset search-all-resources \
  --scope="organizations/123456789012" \
  --asset-types="cloudresourcemanager.googleapis.com/Project" \
  --read-mask="name,labels,createTime" \
  --format="table(name,labels.owner,createTime)"

2.28 Project Creation Controls §

Uncontrolled project creation is how shadow IT arrives on GCP: a project outside your folders inherits none of your policy, and by default any user in your domain can create one.

The controls, applied together:

  1. Remove the domain-wide default grants. Delete roles/resourcemanager.projectCreator and roles/billing.creator from the domain: principal at the organization node (§2.3).
  2. Grant projectCreator only to the factory identity, and only on the folders it should provision into.
  3. Constrain billing. Without roles/billing.user on a billing account, a created project cannot be funded and cannot run billable resources.
  4. Enforce the policy baseline at the org, so even a project created by an exception inherits the guardrails.
  5. Detect. Alert on CreateProject in Admin Activity logs where the parent is not one of your managed folders.
gcloud logging read \
  'protoPayload.methodName="CreateProject"' \
  --organization=123456789012 --limit=20 --freshness=7d \
  --format="table(timestamp,\
    protoPayload.authenticationInfo.principalEmail,\
    resource.labels.project_id)"

Pitfall. Removing projectCreator from the domain does not affect projects that already exist outside your folders. Find them with Cloud Asset Inventory and move them in — a project can be moved between folders with the Resource Manager projects.move method (available in gcloud under the beta component, and in Terraform by changing folder_id).

2.29 Project Deletion and Recovery §

Deleting a project moves it to a DELETE_REQUESTED lifecycle state. Resources stop serving, and after 30 days the project and everything in it are permanently deleted. During the window it can be restored, and it continues to count against your project quota.

gcloud projects delete rc-saas-sbx-alice-01
gcloud projects undelete rc-saas-sbx-alice-01
gcloud projects describe rc-saas-sbx-alice-01 \
  --format="value(lifecycleState)"

What does not come back. The project ID is retired permanently even after a successful restore of a different project; a purged project's ID can never be reused. Some resources — Cloud KMS key versions scheduled for destruction, data past its soft-delete window — are gone independently of the project's state.

Security posture. Deletion is a destructive action available to anyone with resourcemanager.projects.delete. Three defenses:

  • deletion_policy = "PREVENT" in Terraform on every production project, and deletion_protection on folders.
  • An IAM deny policy denying resourcemanager.projects.delete on the production folder to everyone except a break-glass group (§3.17).
  • A high-severity alert on DeleteProject in Admin Activity logs, routed to on-call rather than to a mailbox.

Pitfall. The 30-day window is the only recovery mechanism, and it is not a backup. A project deleted 31 days ago is unrecoverable regardless of support tier. Data whose loss is unacceptable needs its own backup outside the project (§8.27, §9.41).

2.30 Organization Policy §

Organization Policy is the platform-side enforcement point from §1.16: constraints evaluated by the API itself, so they hold regardless of whether a change arrives through Terraform, gcloud, the console, or a client library.

Three kinds of constraint:

  • Managed constraints, named <domain>.managed.<name> — Google-maintained, the current generation, simulated by default in Policy Simulator. Example: constraints/compute.managed.vmExternalIpAccess.
  • Legacy managed constraints (formerly "predefined"), named <domain>.<name> — still valid and still the only form for some controls. Example: constraints/compute.skipDefaultNetworkCreation.
  • Custom constraints, named custom.<Name> — yours, written in CEL against a REST resource type.

Boolean constraints are enforced or not. List constraints carry allowed or denied values, including hierarchy-subtree forms such as under:folders/FOLDER_ID, is:projects/PROJECT_ID, and location groups such as in:us-locations.

The reference baseline, applied at the organization node:

ConstraintSettingPrevents
constraints/iam.allowedPolicyMemberDomainsown customer ID onlygranting roles to outside identities
constraints/iam.managed.disableServiceAccountKeyCreationenforcedlong-lived downloadable credentials
constraints/iam.managed.disableServiceAccountKeyUploadenforcedexternally generated key material
constraints/iam.automaticIamGrantsForDefaultServiceAccountsenforceddefault SAs receiving a basic role
constraints/compute.skipDefaultNetworkCreationenforcedthe permissive default VPC
constraints/compute.managed.vmExternalIpAccessdeny alldirect internet exposure of VMs
constraints/compute.managed.requireOsLoginenforcedmetadata SSH keys bypassing IAM
constraints/compute.managed.disableSerialPortAccessenforcedout-of-band console access
constraints/compute.restrictSharedVpcHostProjectsallowlist of host projectsunapproved network attachment
constraints/storage.publicAccessPreventionenforcedpublic buckets
constraints/storage.uniformBucketLevelAccessenforcedper-object ACLs
constraints/gcp.resourceLocationsin:us-central1-locations, in:europe-west1-locationsdata leaving approved regions
constraints/gcp.restrictServiceUsageallowlistunreviewed services
constraints/essentialcontacts.managed.allowedContactDomainsown domainnotification hijacking

Organizations created on or after 2024-05-03 already have a security baseline enforced automatically, covering the first four iam.* and essentialcontacts.* rows plus constraints/storage.uniformBucketLevelAccess and constraints/compute.managed.restrictProtocolForwardingCreationForTypes. An older organization has none of them and must set all of them explicitly.

Rollout procedure — always dry-run first:

# policy-dryrun.yaml
name: organizations/123456789012/policies/compute.managed.vmExternalIpAccess
dryRunSpec:
  rules:
    - denyAll: true
gcloud org-policies set-policy policy-dryrun.yaml
gcloud org-policies describe compute.managed.vmExternalIpAccess \
  --organization=123456789012 --effective

Violations then appear in Policy Denied audit logs without blocking. Measure for a full change cycle, fix what breaks, then promote dryRunSpec to spec.

resource "google_org_policy_policy" "no_external_ip" {
  name   = "organizations/123456789012/policies/compute.managed.vmExternalIpAccess"
  parent = "organizations/123456789012"

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

Custom constraints cover what Google does not. The shape, with fields as documented:

name: organizations/123456789012/customConstraints/custom.requireDataClassLabel
resourceTypes:
  - cloudresourcemanager.googleapis.com/Project
methodTypes:
  - CREATE
  - UPDATE
condition: "has(resource.labels['data-class'])"
actionType: ALLOW
displayName: Require a data-class label on every project
description: >-
  Projects must carry a data-class label of public, internal,
  confidential, or restricted.
gcloud org-policies set-custom-constraint custom-constraint.yaml
gcloud org-policies list-custom-constraints --organization=123456789012

Required role. roles/orgpolicy.policyAdmin. Grant it to the platform-security group at the organization and to nobody at project scope — a project-scoped org policy admin can relax an inherited constraint for their own project.

Pitfall. A list constraint set at a child replaces the parent's list unless inheritFromParent is true. A team that sets an allowlist on their folder to add one region can silently drop the organization's other restrictions.

2.31 Hierarchical Policy Inheritance §

Three different policy systems flow down the same tree with three different merge rules. Confusing them is the most common source of "why can this principal do that."

SystemMerge ruleCan a child restrict further?Can a child loosen?
IAM allow policiesunion of all ancestorsnoyes (adds)
IAM deny policiesunion; deny wins over allowyesno
Organization policieschild overrides parent unless inheritFromParentyesyes, if it has the admin role
Tagsinherited; child may rebind the same keyn/an/a
Hierarchical firewall policiesevaluated top-down; goto_next continuesyesonly via explicit delegation

The consequences to internalize:

  1. You cannot take away an inherited allow. A role granted at the organization is effective in every project forever. This is why org-node grants must be near-empty (§2.7).
  2. Deny policies are the only subtraction, and they are evaluated before allow policies (§3.17).
  3. Org policy is the only system where a child can be less restrictive than its parent — which is exactly why roles/orgpolicy.policyAdmin is an organization-scoped grant only.

Evaluating what is actually in effect:

gcloud org-policies describe gcp.resourceLocations \
  --project=rc-saas-prod-app-01 --effective
gcloud projects get-iam-policy rc-saas-prod-app-01 \
  --format="table(bindings.role,bindings.members)"

The --effective flag is the important one: it resolves the full ancestry rather than showing only what is set at that node. For IAM, the equivalent hierarchy-wide question is answered by Policy Analyzer (§3.32), because get-iam-policy shows only the bindings set at that resource.

2.32 Resource Hierarchy for SaaS §

The SaaS reference estate (prefix rc-saas) is a multi-tenant product. Its hierarchy optimizes for a fast, uniform delivery pipeline across a small number of environments, with tenant isolation handled inside the application and data layers rather than by the hierarchy.

organizations/123456789012            rickcollette.domain   (SaaS estate)
├── fldr-bootstrap
│   └── rc-saas-bootstrap-seed-01   org-level Terraform, state, factory identity
├── fldr-common
│   ├── rc-saas-shared-net-01       Shared VPC hosts (per-environment VPCs)
│   ├── rc-saas-shared-log-01       aggregated sink destinations
│   ├── rc-saas-shared-sec-01       KMS, Secret Manager, SCC
│   ├── rc-saas-shared-art-01       Artifact Registry
│   └── rc-saas-shared-cicd-01      Cloud Build, Cloud Deploy, WIF pools
├── fldr-environments
│   ├── fldr-development   → rc-saas-dev-app-01,  rc-saas-dev-data-01
│   ├── fldr-nonproduction → rc-saas-stg-app-01,  rc-saas-stg-data-01
│   └── fldr-production    → rc-saas-prod-app-01, rc-saas-prod-data-01
└── fldr-sandbox           → rc-saas-sbx-<user>-01 (TTL 30 days)

Policy placement.

  • Organization: the full baseline (§2.30), the aggregated log sink, Security Command Center.
  • fldr-production: stricter overrides — deny policy on project deletion, Data Access logging required, CMEK required for data services.
  • fldr-sandbox: relaxed where safe (external IPs permitted), hard budget, no Shared VPC attachment.

Why only three environment folders. Every additional environment is a full copy of the policy surface to maintain and a place for drift to hide. The SaaS estate gets preview environments as ephemeral projects in the development folder, created and destroyed by CI, rather than as a permanent fourth tier.

Tenant isolation. Tenants are rows and namespaces, not projects. Where a tenant contractually requires infrastructure isolation, that is the multi-tenant pattern in §2.35, and it is priced accordingly.

2.33 Resource Hierarchy for Enterprise §

The enterprise reference estate (prefix rc-ent) is a traditional enterprise: many business units, mixed compliance regimes, on-premises integration, and real delegation of administration. Its hierarchy carries an extra layer.

organizations/210987654321            ent.rickcollette.domain   (enterprise estate)
├── fldr-bootstrap
├── fldr-common
│   ├── rc-ent-shared-net-01        hub VPC, Interconnect, DNS
│   ├── rc-ent-shared-log-01
│   ├── rc-ent-shared-sec-01
│   └── rc-ent-shared-cicd-01
├── fldr-business-units
│   ├── fldr-bu-manufacturing
│   │   ├── fldr-development  → rc-ent-mfg-dev-app-01
│   │   ├── fldr-nonproduction
│   │   └── fldr-production   → rc-ent-mfg-prod-app-01
│   ├── fldr-bu-logistics     (same three-folder shape)
│   └── fldr-bu-retail        (same three-folder shape)
├── fldr-regulated
│   └── fldr-pci              → rc-ent-pci-prod-app-01  (Assured Workloads)
└── fldr-sandbox

Business unit before environment, not after. The BU folder is where administration is delegated: roles/resourcemanager.folderAdmin for that BU's platform group applies to their whole subtree and nothing else. Environment folders sit inside, so the environment policy is applied per BU and can be audited per BU.

Trade-off. This costs you a policy application per BU rather than one for the whole estate. Mitigate by keeping the baseline at the organization node — every BU inherits it and cannot loosen it without roles/orgpolicy.policyAdmin, which they do not have — and delegating only additive controls downward.

The regulated folder is separate on purpose. PCI or similar scope gets its own subtree with Assured Workloads, its own key management, its own network segment, and no inheritance from a BU folder whose admins are out of scope.

Depth check. organization → fldr-business-units → fldr-bu-X → fldr-production → project is four folder levels, well inside the 10-level limit, and near the practical limit of what a human can audit.

2.34 Multi-Business-Unit Organizations §

Delegation is the whole point of a BU hierarchy, and delegation done badly reintroduces every risk the hierarchy was meant to contain.

What the central platform team keeps, always:

  • roles/orgpolicy.policyAdmin at the organization — the baseline is not negotiable per BU.
  • The aggregated log sink and the logging project.
  • Shared VPC host project administration and firewall policy.
  • KMS administration for keys protecting regulated data.
  • roles/resourcemanager.organizationAdmin.

What a BU platform group receives, scoped to its folder:

  • roles/resourcemanager.folderAdmin on their folder — IAM within their subtree.
  • roles/resourcemanager.projectCreator on their folder.
  • roles/compute.networkUser on the subnets allocated to them.
  • Additive org policies: they may make their subtree more restrictive by setting policies that add denied values, but cannot relax an inherited constraint without the org-scoped admin role.

Cross-BU access is the recurring request and the recurring incident. Handle it with explicit, conditioned grants on the specific resource — a bucket, a dataset, a Pub/Sub topic — never by moving a project or by granting at a shared ancestor. Every cross-BU grant carries an IAM condition with an expiry (§3.19).

Anti-pattern. Giving each BU its own organization. It breaks the aggregated sink, the single Security Command Center view, org-wide policy, and hierarchical firewall policies, and it cannot be undone. One organization with delegated folders is nearly always right.

2.35 Multi-Tenant Organizations §

When tenants require infrastructure-level isolation, the isolation unit is the project, and the hierarchy has to absorb the tenant count.

Three patterns, in increasing cost and isolation:

PatternStructureIsolationPractical ceiling
Shared-everythingone project per environment, tenant as a row/namespaceapplication-levelvery high
Project-per-tenantrc-saas-prod-t<nnnn>-01 under a tenant folderIAM, quota, network, keyshundreds to low thousands, factory-dependent
Folder-per-tenanttenant folder holding that tenant's projectsall of the above plus delegated policylower; folder limits and audit burden bind

Project-per-tenant realities:

  • Project IDs must be generated, so the tenant ordinal must fit inside 30 characters (§2.11).
  • Resource Manager rate limits make bulk onboarding slow; the factory must queue and retry (§2.26).
  • Per-tenant CMEK is the strongest form of tenant separation — a tenant's data becomes cryptographically inaccessible when their key is disabled — and it is why this pattern gets chosen at all.
  • The tenant identifier becomes a tag value, so IAM conditions and org policies can key on it.

Isolation of the control plane matters as much as the data plane. A tenant-scoped support role must be conditioned on the tenant tag, or a support engineer with a folder-level role sees every tenant.

Pitfall. Tenant offboarding is the step that gets skipped. It must delete the project (starting the 30-day clock), disable and schedule destruction of the tenant key, revoke the tenant's federation configuration, and remove the tenant's tag value — with evidence retained in the logging project after the project is gone.

Chapter Summary §

  • Google Cloud has no user database; identities live in Cloud Identity or Google Workspace, and one identity tenant maps to exactly one organization.
  • Unmanaged accounts in your domain look managed and are not; reclaim them before trusting the organization.
  • The organization resource is provisioned automatically, and at creation it grants every domain user Project Creator and Billing Account Creator — remove both immediately.
  • Super Administrator (identity) and Organization Administrator (roles/resourcemanager.organizationAdmin) are different roles and should be held by different people; neither can create projects or folders by default.
  • A billing account is not an IAM parent of a project; billing roles are separate and should be split between finance (viewer, costs manager) and the factory (roles/billing.user).
  • Folders carry policy and delegation, nest up to 10 deep with ≤300 children per parent, and should represent environments and trust zones — not org charts.
  • Project IDs are 6–30 characters, immutable, and never reusable; encode prefix, environment, function, and ordinal so conditions and filters can parse them.
  • Labels are per-resource, uncapped by inheritance, and unusable in policy; tags are discrete resources, inherited, and usable in IAM conditions and organization policy.
  • Production and non-production never share a project; staging must match production's policy, and must not share its keys, secrets, or data.
  • Shared infrastructure, security, logging, networking, and CI/CD each get their own project under a common folder, with consumers receiving the narrowest possible verb.
  • A project factory must emit the project, billing link, labels, tags, APIs, IAM, network attachment, budget, and inventory entry in a single reviewed transaction.
  • Deleting a project starts a 30-day recovery window and nothing more; it is not a backup, and the project ID is retired permanently.
  • Organization policy comes in managed, legacy managed, and custom forms; every new constraint is rolled out dry-run first and promoted after a full change cycle.
  • Organizations created on or after 2024-05-03 inherit a security baseline automatically; older organizations must set the equivalent constraints by hand.
  • IAM allow policies only union downward, only deny policies subtract, and organization policy is the one system where a child can be less restrictive than its parent — which is why roles/orgpolicy.policyAdmin stays at the organization.
  • SaaS hierarchies stay shallow with three environment folders; enterprise hierarchies add a business-unit layer for delegation and keep the baseline central.

Security Checklist §

ControlWhy it mattersHow to verify (CLI + Console)
Domain-restricted sharing enforcedBlocks IAM grants to identities outside your organizationgcloud org-policies describe iam.allowedPolicyMemberDomains --organization=ORG_ID --effective; Console → IAM & Admin → Organization policies
Default org-wide Project Creator / Billing Creator grants removedOtherwise any employee can create ungoverned, funded projectsgcloud organizations get-iam-policy ORG_ID --format="value(bindings.role,bindings.members)"; Console → IAM & Admin → IAM (org scope)
Super admins separated from Organization AdministratorsPrevents one identity from holding both control planesAdmin console → Account → Admin roles; gcloud organizations get-iam-policy ORG_ID
Essential Contacts set for the Security categoryGoogle's compromise and suspension notices must reach someonegcloud essential-contacts list --organization=ORG_ID; Console → IAM & Admin → Essential Contacts
Default network creation suppressedThe default VPC ships with permissive rulesgcloud org-policies describe compute.skipDefaultNetworkCreation --organization=ORG_ID --effective
Service account key creation and upload blockedLong-lived key material is the top credential-leak vectorgcloud org-policies describe iam.managed.disableServiceAccountKeyCreation --organization=ORG_ID --effective
Resource locations restrictedData residency must be enforced, not requestedgcloud org-policies describe gcp.resourceLocations --organization=ORG_ID --effective
Service usage restricted to an allowlistPrevents unreviewed services from holding datagcloud org-policies describe gcp.restrictServiceUsage --organization=ORG_ID --effective
Every project carries the mandatory label setAttribution, detection scoping, and ownership all key on labelsgcloud asset search-all-resources --scope="organizations/ORG_ID" --asset-types="cloudresourcemanager.googleapis.com/Project" --query="NOT labels.data-class:*"
Environment tag bound at the environment foldersTags, unlike labels, drive IAM conditions and org policygcloud resource-manager tags bindings list --parent=//cloudresourcemanager.googleapis.com/projects/PROJECT_NUMBER
Production projects have deletion protectionDeletion has only a 30-day window and no support override afterwardgcloud projects describe PROJECT_ID --format="value(lifecycleState)"; Terraform deletion_policy = "PREVENT"
Aggregated org sink with --include-children existsNew projects must be inside audit coverage from creationgcloud logging sinks describe org-audit-sink --organization=ORG_ID
Shared VPC host projects constrained to an allowlistStops workloads attaching to an unapproved networkgcloud org-policies describe compute.restrictSharedVpcHostProjects --organization=ORG_ID --effective
Every new org policy went dry-run before enforcementEnforcing untested constraints causes outages and rollback pressuregcloud org-policies describe CONSTRAINT --organization=ORG_ID --format="value(dryRunSpec)"
roles/orgpolicy.policyAdmin granted only at the organizationA project-scoped org policy admin can relax inherited guardrailsgcloud asset search-all-iam-policies --scope="organizations/ORG_ID" --query="policy:roles/orgpolicy.policyAdmin"

Sources §