Skip to main content
Multi-Cloud · lesson 2 of 8
Multi-Cloud intermediate Lesson 2 of 8

Also part of: Cloud Security

IAM Concepts That Transfer: AWS vs Azure vs GCP

Grant read-only access to one bucket, done in AWS IAM, Azure RBAC, and GCP IAM side by side — the vocabulary differs, the underlying model doesn't.

By SkillByExample · Published

cloud aws azure gcp iam security

Grant one identity read-only access to one bucket, nothing else. The task is identical on every cloud; only the nouns change.

The vocabulary mapping

ConceptAWSAzureGCP
Who gets accessIAM user / role (ARN)Security principal (user, group, service principal, managed identity — Object ID)Member (user:, serviceAccount:, group:)
What they can doPolicy (JSON, list of allowed actions)Role definition (built-in or custom, list of actions)Role (predefined or custom, list of permissions)
On whatResource ARN in the policy’s Resource fieldScope (a resource ID: subscription / resource group / storage account / container)Resource the binding is attached to (bucket, project, folder)
The act of grantingAttach policy to user/roleCreate a role assignment at a scopeAdd an IAM policy binding on the resource

Same shape every time: principal + role + resource. What’s actually worth learning is this mapping, not the JSON syntax of any one cloud — the syntax is what a reference page gives you; the mapping is what lets you read any of the three cold.

AWS: IAM policy scoped to one bucket

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadOnlyReportsBucket",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::reports-bucket",
        "arn:aws:s3:::reports-bucket/*"
      ]
    }
  ]
}

Both ARNs are required: the bucket ARN itself (without /*) covers s3:ListBucket, and the /* suffix covers s3:GetObject on the objects inside it. Leaving off either one is the most common reason a “read-only” policy still fails with AccessDenied on a list call.

aws iam put-user-policy \
  --user-name analytics-reader \
  --policy-name read-only-reports-bucket \
  --policy-document file://policy.json

aws iam get-user-policy --user-name analytics-reader --policy-name read-only-reports-bucket
$ aws iam get-user-policy --user-name analytics-reader --policy-name read-only-reports-bucket
{
    "UserName": "analytics-reader",
    "PolicyName": "read-only-reports-bucket",
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "ReadOnlyReportsBucket",
                "Effect": "Allow",
                "Action": ["s3:GetObject", "s3:ListBucket"],
                "Resource": ["arn:aws:s3:::reports-bucket", "arn:aws:s3:::reports-bucket/*"]
            }
        ]
    }
}

Azure: role assignment at container scope

Azure ships a built-in role for exactly this — Storage Blob Data Reader — so there’s no policy document to author at all; you assign the existing role at the narrowest scope that covers the container.

az role assignment create \
  --assignee "a1e4f6b2-8c3d-4f19-b7e2-5d9c1a0f3e88" \
  --role "Storage Blob Data Reader" \
  --scope "/subscriptions/3b1f9c2a-6d4e-4a7b-9c11-2f5e8a0b7d33/resourceGroups/learn-rg/providers/Microsoft.Storage/storageAccounts/reportsstore/blobServices/default/containers/reports"
$ az role assignment create --assignee "a1e4f6b2-8c3d-4f19-b7e2-5d9c1a0f3e88" --role "Storage Blob Data Reader" --scope "/subscriptions/.../containers/reports"
{
  "condition": null,
  "conditionVersion": null,
  "id": "/subscriptions/3b1f9c2a-6d4e-4a7b-9c11-2f5e8a0b7d33/.../roleAssignments/2f88a1c4-...",
  "principalId": "a1e4f6b2-8c3d-4f19-b7e2-5d9c1a0f3e88",
  "principalType": "User",
  "roleDefinitionId": "/subscriptions/.../roleDefinitions/2a2b9908-6ea1-4ae2-8e65-a410df84e7d1",
  "roleDefinitionName": "Storage Blob Data Reader",
  "scope": "/subscriptions/3b1f9c2a-6d4e-4a7b-9c11-2f5e8a0b7d33/.../containers/reports"
}

--scope is the whole story here: assign at the storage-account level and the principal can read every container in it, not just reports. Azure RBAC inherits downward through management group -> subscription -> resource group -> resource -> (for storage) container, so the scope you pick is the actual access boundary, not the role name.

GCP: IAM policy binding on the bucket

gcloud storage buckets add-iam-policy-binding gs://reports-bucket \
  --member="user:reader@example.com" \
  --role="roles/storage.objectViewer"
$ gcloud storage buckets add-iam-policy-binding gs://reports-bucket --member="user:reader@example.com" --role="roles/storage.objectViewer"
bindings:
- members:
  - user:reader@example.com
  role: roles/storage.objectViewer
etag: CAE=
version: 1

roles/storage.objectViewer grants storage.objects.get and storage.objects.list — read and list, nothing else — the direct GCP counterpart of the AWS policy and the Azure role above. Read the bucket’s whole policy to confirm what else is already bound there, since add-iam-policy-binding merges into the existing document rather than replacing it:

gcloud storage buckets get-iam-policy gs://reports-bucket
$ gcloud storage buckets get-iam-policy gs://reports-bucket
bindings:
- members:
  - user:reader@example.com
  role: roles/storage.objectViewer
- members:
  - serviceAccount:pipeline@my-project.iam.gserviceaccount.com
  role: roles/storage.objectAdmin
etag: CAI=
version: 1

That second binding is a trap for the unwary: roles/storage.objectAdmin looks like a sibling of objectViewer but grants full read/write/delete. GCP’s flat “list of bindings” model makes it easy to see everyone with access to a resource in one call — which is an advantage AWS’s per-principal policies and Azure’s per-assignment model don’t give you as directly.

The one thing that doesn’t map cleanly

AWS policies can be explicit deny or implicit deny (anything not allowed is denied, and an explicit Deny always wins over an Allow). Azure and GCP both support explicit deny too (Azure via deny assignments, GCP via IAM Deny policies), but reach for it far less often in practice — the idiomatic pattern on all three is still additive, least-privilege grants, not deny rules layered on top of broad ones.

Practice

1. Write the AWS policy that grants read-only access to a single *prefix* inside a bucket (`reports-bucket/2026/`), not the whole bucket.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::reports-bucket",
      "Condition": { "StringLike": { "s3:prefix": "2026/*" } }
    },
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::reports-bucket/2026/*"
    }
  ]
}

ListBucket still targets the bucket ARN — you can’t scope a list call to a prefix without a Condition — while GetObject scopes naturally through the resource ARN’s path.

2. Find the built-in Azure role that allows write but not delete on blob data.
az role definition list --name "Storage Blob Data Contributor" --query "[].permissions"

There isn’t one. Storage Blob Data Contributor grants read, write, and delete as a single bundle — Azure has no built-in “read-write-no-delete” blob role. Getting that exact permission set means authoring a custom role definition.

3. List every principal with any role on the GCP bucket, grouped by role.
gcloud storage buckets get-iam-policy gs://reports-bucket --format=json | jq '.bindings'

This is the query from the lesson body, pulled apart by role — useful as an access review before adding one more binding to a bucket you didn’t set up yourself.

4. Revoke the access granted in each of the three examples above.
aws iam delete-user-policy --user-name analytics-reader --policy-name read-only-reports-bucket

az role assignment delete --assignee "a1e4f6b2-8c3d-4f19-b7e2-5d9c1a0f3e88" \
  --role "Storage Blob Data Reader" --scope "/subscriptions/.../containers/reports"

gcloud storage buckets remove-iam-policy-binding gs://reports-bucket \
  --member="user:reader@example.com" --role="roles/storage.objectViewer"

Three different verbs — delete-user-policy, role assignment delete, remove-iam-policy-binding — for the same conceptual undo.

Next: the same 4 vCPU / 16 GB workload, priced and named on all three clouds — what m5.xlarge, Standard_D4s_v5, and n2-standard-4 actually mean.

Frequently Asked Questions

Is AWS IAM the same model as Azure RBAC and GCP IAM?
The underlying model is identical: a principal is granted a role at a resource scope. The vocabulary differs — AWS attaches policies to users/roles, Azure assigns role definitions to security principals at a scope, GCP binds members to roles on a resource — but all three answer the same question: who, can do what, on which resource.
What is the equivalent of an AWS IAM policy in Azure?
A role definition (built-in or custom), assigned to a principal via a role assignment. The policy document's list of allowed actions maps to the role definition's `actions` array; the resource ARN maps to the assignment's `scope`.
Why does GCP call it a role 'binding' instead of an assignment?
Because GCP's IAM policy is a single document per resource — a list of bindings, each pairing one role with a list of members — rather than separate objects like AWS's attached policies or Azure's role assignments. Adding access means adding a member to an existing binding or creating a new one in that same policy document.
Which of the three is least likely to accidentally grant write access?
None of them protect you by default — all three have a real read-only role (`s3:GetObject`/`s3:ListBucket` only, `Storage Blob Data Reader`, `roles/storage.objectViewer`) and all three also have broader roles with deceptively similar names (`Storage Blob Data Contributor`, `roles/storage.objectAdmin`) that grant write and delete. Reading the exact role name, not just picking the first result, is the actual safeguard on every cloud.