04 · Advanced IAM (Org Policies, Custom Roles)¶
Levels 1-2 used predefined roles like roles/storage.objectViewer. At
organization scale you also need custom roles for least-privilege
grants predefined roles don't match, and Organization Policies to set
guardrails that apply across every project underneath an org or folder,
regardless of who holds Owner on any individual project.
Custom roles¶
gcloud iam roles create orderServiceDeployer \
--project=my-project \
--title="Order Service Deployer" \
--description="Deploy and view logs for the order service, nothing else" \
--permissions=run.services.get,run.services.update,run.revisions.list,logging.logEntries.list \
--stage=GA
gcloud iam roles describe orderServiceDeployer --project=my-project
# name: projects/my-project/roles/orderServiceDeployer
# includedPermissions:
# - logging.logEntries.list
# - run.revisions.list
# - run.services.get
# - run.services.update
# stage: GA
Grant it exactly like a predefined role:
gcloud projects add-iam-policy-binding my-project \
--member="user:dev@example.com" \
--role="projects/my-project/roles/orderServiceDeployer"
Gotcha — custom roles don't auto-update with new GCP permissions.
Predefined roles get new permissions added by Google as services evolve;
a custom role is a frozen permission list you own and must maintain
yourself. Review custom roles periodically against
gcloud iam roles describe roles/run.developer (or whichever predefined
role you modeled it on) to catch drift.
Gotcha — testing permission sets before granting. Use
gcloud iam roles create --stage=TESTING for a role you're still tuning; a
TESTING role behaves identically for grants but is excluded from
recommender suggestions and flagged as provisional in audit tooling, so
reviewers know not to treat it as final.
Organization Policies¶
Org Policies are constraints, not permissions — they restrict what's possible, even for a project Owner. They apply at org, folder, or project level and inherit downward.
# List all available constraints
gcloud org-policies list --organization=123456789012 | head -5
# Restrict VM external IPs org-wide
gcloud org-policies set-policy external-ip-policy.yaml --organization=123456789012
# external-ip-policy.yaml
name: organizations/123456789012/policies/compute.vmExternalIpAccess
spec:
rules:
- denyAll: true
gcloud org-policies describe compute.vmExternalIpAccess \
--organization=123456789012
# spec:
# rules:
# - denyAll: true
A VM creation that would assign an external IP now fails at admission time
— ERROR: (gcloud.compute.instances.create) ... Constraint
constraints/compute.vmExternalIpAccess violated — no matter who runs it or
what project-level IAM they hold.
Gotcha — inheritance can be overridden per-folder, deliberately or by
accident. A child folder/project can set its own policy that overrides an
inherited one (unless the parent constraint is marked non-overridable at
the org level). This is intentional for exceptions (e.g., a
public-demos folder that needs external IPs), but it means "we set an org
policy" doesn't guarantee it holds everywhere — check effective policy per
resource with gcloud org-policies describe CONSTRAINT --project=X and
compare against the org-level setting.
Common constraints worth knowing¶
| Constraint | Effect |
|---|---|
compute.vmExternalIpAccess |
Block/allow list which VMs may have external IPs. |
iam.disableServiceAccountKeyCreation |
Forbid creating downloadable SA JSON keys org-wide. |
compute.requireOsLogin |
Force OS Login instead of SSH metadata keys for VM access. |
compute.restrictVpcPeering |
Limit which VPCs/projects may peer with each other. |
gcp.resourceLocations |
Restrict which regions resources may be created in (data residency). |
iam.allowedPolicyMemberDomains |
Restrict IAM grants to specific identity domains (block gmail.com principals in a corp org). |
IAM Conditions recap and denial policies¶
Level 1-2 covered basic role bindings. IAM Deny policies are the complement to Allow bindings — they take precedence regardless of any Allow grant, useful for hard org-wide exclusions like "nobody may delete production BigQuery datasets, even Owners":
gcloud iam deny-policies create no-prod-dataset-delete \
--organization=123456789012 \
--policy-file=deny-policy.yaml
# deny-policy.yaml
displayName: "no-prod-dataset-delete"
rules:
- denyRule:
deniedPrincipals:
- "principalSet://goog/public:all"
deniedPermissions:
- "bigquery.datasets.delete"
denialCondition:
expression: 'resource.name.startsWith("projects/prod-")'
Gotcha — Deny policies evaluate before Allow, always. If a Deny policy matches, no Allow binding can override it, including an Owner role — the only fix is to edit or delete the Deny policy itself. This makes Deny policies powerful and also easy to lock yourself out with; test in a non-prod org node first.
Cheat sheet¶
| Command | Purpose |
|---|---|
gcloud iam roles create |
Define a custom role with an exact permission set. |
gcloud org-policies set-policy |
Apply a constraint at org/folder/project scope. |
gcloud org-policies describe |
Check the effective policy for a constraint on a resource. |
gcloud iam deny-policies create |
Create a hard deny that overrides any Allow binding. |
--stage=TESTING |
Mark a custom role as provisional during tuning. |
How It Actually Works¶
Custom roles and organization policies operate on two different planes of the same permission system. A custom role is just a named bundle of permissions you assemble from the same catalog predefined roles draw from — binding it to a member is evaluated by the exact same hierarchy- walking Checker service as any built-in role, so custom roles don't change how access is granted, only what the granted bundle contains. Organization policy constraints are structurally different: they don't grant or deny specific principals anything — they restrict what configurations are allowed to exist at all (e.g., "no VM may have an external IP"), enforced at resource-creation or resource-update time regardless of who's making the call or what IAM role they hold, which is why an Owner can still be blocked from creating a resource that violates an org policy. IAM Deny policies bridge the two: like org policies they're evaluated ahead of any Allow grant and can't be overridden by role bindings, but like IAM roles they target specific principals and specific permissions rather than resource configuration shapes.
Exercise¶
Create a custom role with exactly the four permissions needed to view and
restart a Cloud Run revision (no update/delete). Grant it to a test user.
Separately, write and apply an org-policy YAML that denies VM external IPs
at your test project (project-level override is fine without an actual
org), attempt to create a VM with --no-address omitted, and confirm the
constraint violation error. Then describe the effective policy to prove
it's active.