🔐 Cloud IAM Essentials: Who Can Do What, and Where

The model behind access control on Azure, AWS, Google Cloud and Oracle Cloud (principals, roles, policies and scopes), with each cloud's evaluation rules modelled and run, roles for machines instead of keys, and a least-privilege recipe

💡 How this page was made. The evaluation models were run for real as small Python programs on 6 October 2026 (nothing was downloaded). They are my own simplified models of each cloud's rules, written to show how decisions are reached: they are not the clouds' real engines, and the real services have many more features. The rules they model come from the providers' documentation, read on 6 October 2026. Commands that need a cloud account are marked from the documentation, not run here. Related pages: the account pages (Azure, AWS, Google Cloud, Oracle) introduce each cloud's identity system, and the networking page covers the firewalls (security groups and their cousins).

The one question

Every access system answers the same question for every request: may this principal perform this action on this resource? Three nouns and one default:

How the four clouds name things:

Principals: people and programs

People should sign in through your identity provider (the company directory, or the cloud's own: Entra ID, IAM Identity Center, Google Account or Workspace, an OCI identity domain), and get temporary access. Groups are how you give access to many people at once: give the group the access, and move people in and out of the group.

Programs need identities too, and this is where most breaches begin, because the easy way is a long-lived secret in a file. Each cloud has a better way, where the machine itself is the identity:

The rule everywhere: no keys for machines on the cloud, short-lived tokens for everything else. The CLI pages show the sign-in methods for people.

How a policy looks

AWS: JSON statements

A permissions policy says what is allowed. A role also has a trust policy that says who may assume it. Both are JSON. This is the trust policy that lets EC2 machines use a role, parsed by Python to prove it is valid JSON, followed by a broken version (a trailing comma, the commonest mistake):

>>> import json
>>> trust = '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Principal": {"Service": "ec2.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'
>>> doc = json.loads(trust)
>>> doc['Statement'][0]['Principal']
{'Service': 'ec2.amazonaws.com'}
>>> doc['Statement'][0]['Action']
'sts:AssumeRole'
>>> bad = '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": "s3:GetObject",}]}'
>>> json.loads(bad)
Traceback (most recent call last):
  [...]
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 86 (char 85)

The permissions side of the same idea, written the usual way (from the documentation, not run here):

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

It reads: allow these two actions on this bucket and its objects. Anything else is denied by default.

Azure, Google Cloud and Oracle: assignments, bindings, sentences

How decisions are made: four models

AWS: allow, but explicit deny wins

From AWS's documentation: for a request inside one account, the identity-based and resource-based policies are combined as a union (an allow in either grants access) and an explicit deny in any policy overrides every allow. A permissions boundary or an organization policy (SCP) is a limit: the result is the intersection, so the action must be allowed by both. The model:

>>> from fnmatch import fnmatch
>>> def matches(stmt, action, resource):
...     acts = stmt['Action'] if isinstance(stmt['Action'], list) else [stmt['Action']]
...     ress = stmt['Resource'] if isinstance(stmt['Resource'], list) else [stmt['Resource']]
...     return any(fnmatch(action, a) for a in acts) and any(fnmatch(resource, r) for r in ress)
... 
>>> def evaluate(policies, action, resource, boundary=None):
...     stmts = [s for p in policies for s in p]
...     if any(s['Effect'] == 'Deny' and matches(s, action, resource) for s in stmts):
...         return 'DENY (explicit deny always wins)'
...     allowed = any(s['Effect'] == 'Allow' and matches(s, action, resource) for s in stmts)
...     if not allowed: return 'DENY (default: nothing allowed it)'
...     if boundary is not None and not any(s['Effect'] == 'Allow' and matches(s, action, resource) for s in boundary):
...         return 'DENY (outside the permissions boundary)'
...     return 'ALLOW'
... 
>>> reader = [{'Effect': 'Allow', 'Action': ['s3:Get*', 's3:List*'], 'Resource': '*'}]
>>> nodelete = [{'Effect': 'Deny', 'Action': 's3:Delete*', 'Resource': 'arn:aws:s3:::prod-*'}]
>>> extra = [{'Effect': 'Allow', 'Action': 's3:*', 'Resource': '*'}]
>>> evaluate([reader], 's3:GetObject', 'arn:aws:s3:::lab-data/a.txt')
'ALLOW'
>>> evaluate([reader], 's3:PutObject', 'arn:aws:s3:::lab-data/a.txt')
'DENY (default: nothing allowed it)'
>>> evaluate([reader], 'ec2:RunInstances', '*')
'DENY (default: nothing allowed it)'
>>> evaluate([extra, nodelete], 's3:DeleteObject', 'arn:aws:s3:::prod-data/a.txt')
'DENY (explicit deny always wins)'
>>> evaluate([extra, nodelete], 's3:DeleteObject', 'arn:aws:s3:::lab-data/a.txt')
'ALLOW'
>>> evaluate([extra], 's3:PutObject', 'arn:aws:s3:::lab-data/a.txt', boundary=reader)
'DENY (outside the permissions boundary)'
>>> evaluate([extra], 's3:GetObject', 'arn:aws:s3:::lab-data/a.txt', boundary=reader)
'ALLOW'

Read the results:

Azure: additive roles on a hierarchy, and deny assignments

Microsoft's documentation: roles are additive, "your effective permissions are the sum of your role assignments", assignments at a scope apply to everything below (management group, subscription, resource group, resource), and a deny assignment, if one applies, blocks access before anything else is considered. The model, with Ana as Reader on the whole subscription and Contributor on one resource group, and Ben as Owner of that group:

>>> roles = {'Reader': {'read'}, 'Contributor': {'read', 'write', 'delete'}, 'Owner': {'read', 'write', 'delete', 'assign'}}
>>> assignments = [('ana', 'Reader', '/subscriptions/s1'), ('ana', 'Contributor', '/subscriptions/s1/resourceGroups/lab-rg'), ('ben', 'Owner', '/subscriptions/s1/resourceGroups/lab-rg')]
>>> denies = [('ben', 'delete', '/subscriptions/s1/resourceGroups/lab-rg/providers/vm/prod-vm')]
>>> def inside(resource, scope): return resource == scope or resource.startswith(scope + '/')
... 
>>> def azure_allowed(who, action, resource):
...     for d_who, d_act, d_scope in denies:
...         if d_who == who and d_act == action and inside(resource, d_scope): return 'DENIED by a deny assignment'
...     granted = set()
...     for a_who, role, scope in assignments:
...         if a_who == who and inside(resource, scope): granted |= roles[role]
...     return 'ALLOWED' if action in granted else 'not allowed (no role grants it here)'
... 
>>> rg = '/subscriptions/s1/resourceGroups/lab-rg'
>>> azure_allowed('ana', 'write', rg + '/providers/vm/test-vm')
'ALLOWED'
>>> azure_allowed('ana', 'write', '/subscriptions/s1/resourceGroups/other-rg/providers/vm/x')
'not allowed (no role grants it here)'
>>> azure_allowed('ana', 'read', '/subscriptions/s1/resourceGroups/other-rg/providers/vm/x')
'ALLOWED'
>>> azure_allowed('ben', 'delete', rg + '/providers/vm/test-vm')
'ALLOWED'
>>> azure_allowed('ben', 'delete', rg + '/providers/vm/prod-vm')
'DENIED by a deny assignment'
>>> azure_allowed('ben', 'assign', '/subscriptions/s1')
'not allowed (no role grants it here)'

Google Cloud: allow policies inherited downward, deny policies first

Google's documentation: allow policies inherit down the hierarchy (organization, folder, project, resource), and "IAM always checks relevant deny policies before checking relevant allow policies": a deny at any level stays in effect below it, and lower levels cannot override it. A deny rule lists denied principals, exception principals and denied permissions. The model:

>>> parent = {'project:lab': 'folder:dev', 'folder:dev': 'org:acme', 'org:acme': None}
>>> allow = {'org:acme': [('group:admins', 'owner')], 'folder:dev': [('group:devs', 'viewer')], 'project:lab': [('user:ana', 'editor')]}
>>> perms = {'viewer': {'compute.get', 'compute.list'}, 'editor': {'compute.get', 'compute.list', 'compute.create', 'compute.delete'}, 'owner': {'compute.get', 'compute.list', 'compute.create', 'compute.delete', 'iam.set'}}
>>> groups = {'user:ana': {'group:devs'}, 'user:cy': {'group:devs'}}
>>> deny = {'folder:dev': [('compute.delete', {'group:admins'})]}
>>> def chain(node):
...     while node: yield node; node = parent[node]
... 
>>> def google_check(who, perm, project):
...     me = {who} | groups.get(who, set())
...     for node in chain(project):
...         for denied_perm, exceptions in deny.get(node, []):
...             if denied_perm == perm and who not in exceptions and not (me & exceptions): return 'DENIED by a deny policy on ' + node
...     for node in chain(project):
...         for member, role in allow.get(node, []):
...             if member in me and perm in perms[role]: return 'ALLOWED by %s on %s' % (role, node)
...     return 'denied (no role grants it)'
... 
>>> google_check('user:ana', 'compute.create', 'project:lab')
'ALLOWED by editor on project:lab'
>>> google_check('user:cy', 'compute.list', 'project:lab')
'ALLOWED by viewer on folder:dev'
>>> google_check('user:cy', 'compute.create', 'project:lab')
'denied (no role grants it)'
>>> google_check('user:ana', 'compute.delete', 'project:lab')
'DENIED by a deny policy on folder:dev'

Oracle Cloud: allow-only sentences with inheritance

Oracle's documentation: a policy attached to a compartment (or the tenancy) applies to child compartments, there is no deny, and the four verbs include the lower ones (manage includes use, read and inspect). The model parses real policy sentences and checks requests:

>>> import re
>>> VERBS = ['inspect', 'read', 'use', 'manage']
>>> STATEMENTS = ['Allow group Developers to manage instance-family in compartment lab', 'Allow group Auditors to read all-resources in tenancy', 'Allow group Interns to inspect all-resources in compartment lab']
>>> def parse(s):
...     m = re.match(r'Allow group (\S+) to (\w+) (\S+) in (tenancy|compartment (\S+))$', s)
...     return {'group': m.group(1), 'verb': m.group(2), 'type': m.group(3), 'where': m.group(5) or 'tenancy'}
... 
>>> policy = [parse(s) for s in STATEMENTS]
>>> policy[0]
{'group': 'Developers', 'verb': 'manage', 'type': 'instance-family', 'where': 'lab'}
>>> FAMILY = {'instance-family': {'instances', 'instance-pools'}}
>>> def covers(where, comp): return where == 'tenancy' or where == comp or comp.startswith(where + ':')
... 
>>> def oci_check(group, verb, rtype, comp):
...     for p in policy:
...         if p['group'] == group and covers(p['where'], comp) and (p['type'] == rtype or p['type'] == 'all-resources' or rtype in FAMILY.get(p['type'], ())) and VERBS.index(p['verb']) >= VERBS.index(verb):
...             return 'ALLOWED by: %s %s in %s' % (p['verb'], p['type'], p['where'])
...     return 'denied (no statement allows it)'
... 
>>> oci_check('Developers', 'use', 'instances', 'lab')
'ALLOWED by: manage instance-family in lab'
>>> oci_check('Developers', 'use', 'instances', 'lab:test')
'ALLOWED by: manage instance-family in lab'
>>> oci_check('Developers', 'use', 'instances', 'production')
'denied (no statement allows it)'
>>> oci_check('Auditors', 'read', 'buckets', 'lab:test')
'ALLOWED by: read all-resources in tenancy'
>>> oci_check('Auditors', 'manage', 'buckets', 'lab')
'denied (no statement allows it)'
>>> oci_check('Interns', 'read', 'instances', 'lab')
'denied (no statement allows it)'

Giving a machine access, without a key (from the documentation, not run here)

  aws iam create-role --role-name web-role --assume-role-policy-document file://trust.json
  aws iam attach-role-policy --role-name web-role --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
  az role assignment create --assignee PRINCIPAL_ID --role "Storage Blob Data Reader" --scope /subscriptions/SUB/resourceGroups/lab-rg
  gcloud iam service-accounts create web-sa
  gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:web-sa@PROJECT_ID.iam.gserviceaccount.com" --role="roles/storage.objectViewer"
  gcloud compute instances create web1 --service-account=web-sa@PROJECT_ID.iam.gserviceaccount.com

Do not rely on the default Compute Engine service account: Google's documentation says the default accounts automatically receive the Editor role, and recommends disabling that.

  Allow dynamic-group web-servers to read objects in compartment lab

Names of roles (AmazonS3ReadOnlyAccess, Storage Blob Data Reader, roles/storage.objectViewer) are examples: look up the current ones before use.

A least-privilege recipe

  1. Start from the job, not the person: "this application reads from one bucket".
  2. Pick the narrowest built-in role that does it (AWS managed policies, Azure built-in roles, Google predefined roles, Oracle's smallest verb). The documentation of AWS and Google both recommend starting from managed or predefined roles and narrowing.
  3. Scope it small: a bucket or a resource group or one compartment, not the account.
  4. Prefer groups and roles to individuals.
  5. Add what the errors ask for, one permission at a time, rather than starting with admin and "tightening later". Later never comes.
  6. Review and remove. AWS provides last accessed information for users, roles and permissions to show what is unused, and its documentation says to remove it regularly. The other clouds have similar tools.
  7. Log everything. AWS CloudTrail, Azure's activity logs, Google Cloud Audit Logs and Oracle's audit service record who did what.
  8. Keep an emergency path: a tightly controlled, MFA-protected break-glass account, tested before you need it.

Common mistakes

What this page did not cover

Conditions (time, source address, tags), federation and SSO setup, cross-account and cross-tenant access, organization-wide guardrails (AWS SCPs, Azure Policy, Google organization policies), secrets management and privileged access tools. Each cloud's IAM documentation covers them.

The cheat sheet

← Cloud Networking Essentials: Addresses, Subnets, Routes and Rules · ↑ All tutorials · Containers on the Cloud: Images, Registries and Serverless Containers →