GCP for DevOps — IAM Policies

4 minute read

IAM Policies

An IAM policy is a collection of bindings that associate one or more members with a role on a resource. Policies are the mechanism through which you grant access in GCP.

Policy Structure

┌──────────────────────────────────────────────┐
│              IAM Policy                      │
│                                              │
│  binding 1:                                  │
│    role: roles/storage.objectViewer          │
│    members:                                  │
│      - user:alice@example.com                │
│      - group:devops@example.com              │
│                                              │
│  binding 2:                                  │
│    role: roles/storage.objectAdmin           │
│    members:                                  │
│      - serviceAccount:deploy@proj.iam...     │
│    condition:                                │
│      title: "Business hours only"            │
│      expression: request.time.getHours(      │
│        "America/New_York") >= 9 &&           │
│        request.time.getHours(                │
│        "America/New_York") <= 17             │
│                                              │
│  etag: BwXyz123=                             │
│  version: 3                                  │
└──────────────────────────────────────────────┘

Viewing IAM Policies

# View the IAM policy for a project
$ gcloud projects get-iam-policy my-project --format=yaml

bindings:
- members:
  - user:admin@example.com
  role: roles/owner
- members:
  - serviceAccount:ci-deployer@my-project.iam.gserviceaccount.com
  role: roles/container.developer
- members:
  - group:devops-team@example.com
  role: roles/viewer
etag: BwXyz123=
version: 1

# View policy for a specific resource (bucket)
$ gcloud storage buckets get-iam-policy gs://my-bucket --format=yaml

# View policy for a Compute Engine instance
$ gcloud compute instances get-iam-policy my-vm --zone=us-central1-a

Adding IAM Bindings

The add-iam-policy-binding command is the safest way to add a binding — it’s additive and won’t remove existing bindings.

# Grant a user a role on a project
$ gcloud projects add-iam-policy-binding my-project \
    --member="user:bob@example.com" \
    --role="roles/compute.viewer"

Updated IAM policy for project [my-project].

# Grant a group a role on a bucket
$ gcloud storage buckets add-iam-policy-binding gs://my-bucket \
    --member="group:data-team@example.com" \
    --role="roles/storage.objectViewer"

# Grant a service account a role on a resource
$ gcloud projects add-iam-policy-binding my-project \
    --member="serviceAccount:app-sa@my-project.iam.gserviceaccount.com" \
    --role="roles/cloudsql.client"

Removing IAM Bindings

# Remove a specific binding
$ gcloud projects remove-iam-policy-binding my-project \
    --member="user:bob@example.com" \
    --role="roles/compute.viewer"

Updated IAM policy for project [my-project].

IAM Conditions

Conditions add contextual restrictions to a binding. They use Common Expression Language (CEL).

# Grant access only during business hours
$ gcloud projects add-iam-policy-binding my-project \
    --member="user:dev@example.com" \
    --role="roles/compute.instanceAdmin.v1" \
    --condition='expression=request.time.getHours("America/New_York") >= 9 && request.time.getHours("America/New_York") <= 17,title=Business hours only,description=Allow access only during business hours EST'

# Grant access only to resources with a specific prefix
$ gcloud projects add-iam-policy-binding my-project \
    --member="serviceAccount:ci@my-project.iam.gserviceaccount.com" \
    --role="roles/storage.objectAdmin" \
    --condition='expression=resource.name.startsWith("projects/_/buckets/my-bucket/objects/deploy/"),title=Deploy bucket only'

Useful Condition Expressions

Condition CEL Expression
Time-based request.time.getHours("UTC") >= 9 && request.time.getHours("UTC") <= 17
Resource name prefix resource.name.startsWith("projects/_/buckets/my-bucket")
Resource type resource.type == "storage.googleapis.com/Bucket"
Temporary access request.time < timestamp("2026-06-30T00:00:00Z")
IP-based (VPC SC) Use VPC Service Controls instead

Policy Inheritance

IAM policies are inherited down the resource hierarchy. A binding at the organization level applies to all folders, projects, and resources below it.

Organization (roles/viewer → group:auditors@)
    │
    ├── Folder: Production
    │   │   (roles/compute.viewer → inherited)
    │   │
    │   ├── Project: prod-web
    │   │   (roles/compute.viewer → inherited from org AND folder)
    │   │
    │   └── Project: prod-api
    │       (roles/compute.viewer → inherited)
    │
    └── Folder: Development
        │
        └── Project: dev-sandbox
            (roles/compute.viewer → inherited from org)

Key Rule: You cannot remove an inherited binding at a lower level. To restrict access, use deny policies or organization policies.

Deny Policies

Deny policies explicitly block access, regardless of any allow policies. They take precedence over allow bindings.

# Create a deny policy (using REST API / Terraform — no gcloud support yet)
# Example: Deny deletion of production resources
{
  "displayName": "Deny production deletion",
  "rules": [
    {
      "denyRule": {
        "deniedPrincipals": ["principalSet://goog/group/developers@example.com"],
        "deniedPermissions": [
          "compute.instances.delete",
          "storage.buckets.delete",
          "container.clusters.delete"
        ],
        "exceptionPrincipals": ["principal://goog/subject/admin@example.com"],
        "denialCondition": {
          "expression": "resource.matchTag('env', 'production')"
        }
      }
    }
  ]
}

Policy Evaluation Order

Request arrives
    │
    ├── 1. Check DENY policies (org → project → resource)
    │       If denied → REJECT ❌
    │
    ├── 2. Check ALLOW policies (resource → project → folder → org)
    │       If any binding matches → ALLOW ✅
    │
    └── 3. No match → DENY (default deny) ❌

Best Practices for IAM Policies

Practice Description
Use groups, not individual users group:devops@ instead of user:alice@
Grant at the narrowest scope Project-level instead of org-level
Use conditions for temporary access Set expiration dates
Audit with Policy Analyzer Identify who has access to what
Enable Cloud Audit Logs Track all IAM changes
Use IAM Recommender Remove unused permissions

Testing IAM Permissions

# Check if a member has specific permissions
$ gcloud asset analyze-iam-policy \
    --organization=123456789 \
    --identity="user:alice@example.com" \
    --full-resource-name="//compute.googleapis.com/projects/my-project/zones/us-central1-a/instances/my-vm" \
    --permissions="compute.instances.delete"

# Simpler: test your own permissions
$ gcloud projects test-iam-permissions my-project \
    --permissions="compute.instances.create,compute.instances.delete"

permissions:
- compute.instances.create

Common Mistakes

Mistake Why It’s Wrong Fix
Using set-iam-policy to add a binding Overwrites the entire policy Use add-iam-policy-binding
Granting roles to individual users Hard to audit and maintain Use Google Groups
Not using etag with set-iam-policy Race conditions in concurrent updates Always include etag
Ignoring inherited permissions Users may have broader access than expected Audit with Policy Analyzer

Quick Reference

Command Description
gcloud projects get-iam-policy PROJECT View project IAM policy
gcloud projects add-iam-policy-binding PROJECT --member=M --role=R Add a binding
gcloud projects remove-iam-policy-binding PROJECT --member=M --role=R Remove a binding
gcloud projects add-iam-policy-binding PROJECT --member=M --role=R --condition=EXPR Add conditional binding
gcloud asset analyze-iam-policy --organization=ORG --identity=USER Analyze effective access

Next → Workload Identity

Updated: