Why architecture matters here
IAM is a control plane; it does not fail dramatically — it drifts. Least privilege at day one becomes over-privileged at year three because roles accumulate permissions and nobody removes them. Attackers find the accumulated privilege and pivot.
The architecture matters because prevention is the layered defense. Boundaries cap what a role can do even if its policy is expanded. SCPs cap what an entire org can do. ABAC scopes access by tag. Sessions add temporal limits. Audit trail catches drift.
Understanding the layers means designing them together instead of hoping any single one holds.
The architecture: every piece explained
The top strip is identity. Human users federate through SSO — no local passwords, no long-lived keys. Workload identity lets services assume roles via short-lived tokens (STS, workload identity federation, GKE workload identity). Groups + roles aggregate permissions by function; users assume roles rather than getting direct grants. Policies are JSON allow/deny statements attached to identities or resources.
The middle row is the constraint layers. Permission boundary caps the maximum a role can effectively be granted; a delegated admin can grant only within this boundary. Service Control Policies apply org-wide guardrails (e.g., no root user, no public S3 buckets). Conditions + ABAC restrict access by tag — role X can only access resources tagged with the same cost-center. Session controls require MFA and cap duration.
The lower rows are lifecycle and observation. Access review quarterly re-attests role assignments and permissions. Audit trail streams every API call to a central log store for detection. Ops defines break-glass procedures for emergency access, drift detection to find over-privilege, and threat detection to catch abuse.
Principals - the things that ask for access
Every decision starts with a principal: an authenticated identity the platform can name in a policy. Four kinds behave differently enough that conflating them causes most IAM messes. Human identities should not live in the cloud provider at all. Federate an external identity provider in, so joiners and leavers are handled once by HR-driven provisioning rather than in every account. The federated result is an assumed role session on AWS, an IAM principal on GCP, an Entra ID assignment on Azure. Permissions attach to a group, never a person - a group is the only place where revoking access is one edit.
Service accounts (Azure: service principals) are non-human identities. In GCP a service account is itself a resource with its own IAM policy naming who may impersonate it, a second permission surface people forget to review; AWS has only a role whose trust policy says who may assume it. Workload identities bind such an account to a runtime location instead of a key - an instance profile, a GKE pod service account, an Azure managed identity - so nothing is stored. An external principal is a vendor or CI provider, and always needs conditions.
Permissions, roles, and bindings are three different objects
A permission is a vendor-defined verb on a resource type, never authored by you: s3:GetObject, storage.objects.get, Microsoft.Storage/storageAccounts/blobServices/containers/read. A role is a named bundle of them - GCP predefined and custom roles, Azure role definitions with their Actions and NotActions lists, AWS managed and customer-managed policy documents. A binding is the triple that actually grants: principal, role, scope.
The vendors disagree about where scope lives, which explains most cross-cloud confusion. In GCP and Azure the scope is the attachment point in the resource hierarchy: a GCP allow policy is a list of bindings stored on an organization, folder, project or resource, and an Azure role assignment names a scope in the same kind of tree. In AWS the attachment point is the identity and the scope is text inside the document, in the Resource element - which is why AWS over-privilege looks like a wildcard rather than a binding made too high in the tree. AWS also makes a role an identity you can become, whereas a GCP or Azure role is only a bundle.
How a request is actually evaluated
The model is small enough to memorise, and memorising it settles most "why is this denied" arguments. Access is deny by default. Allows are a union: identity policies, group and hierarchy inheritance, and resource policies all add. An explicit deny is a veto no later allow overturns. Ceilings are an intersection - they never grant, only cap.
decide(principal, action, resource, context):
1. explicit DENY anywhere in scope
(identity | resource | org guardrail) -> DENY, final
2. org guardrail (SCP / deny policy) allows -> else DENY
3. permission boundary allows -> else DENY
4. explicit ALLOW from an identity policy
or from the resource's own policy -> ALLOW
5. nothing matched -> DENY (default)Inheritance direction matters. GCP allow policies flow downward and are purely additive, so you cannot subtract at a project what a folder granted; that needs an IAM deny policy or an Organization Policy constraint, two distinct mechanisms. Azure role assignments inherit the same way, and although deny assignments exist they are created by platform-managed services rather than authored freely, so Azure Policy is the guardrail you write. AWS identity policies do not inherit at all - the hierarchy effect comes entirely from Service Control Policies. Evaluate every change in the vendor's policy simulator; reading a document is not evaluating it.
RBAC, ABAC, and condition keys
Pure RBAC scales as roles multiplied by scopes: three environments, twenty teams and a dozen services produce hundreds of near-identical role definitions that nobody deletes. ABAC collapses that - the decision reads attributes of the principal, the resource and the request, so one policy covers every team following the tagging convention. Condition keys come in four families: principal attributes (tags, group, organization id), resource attributes (tags, project, name prefix), request context (source IP, private endpoint, MFA, time), and provenance - which service is calling on the principal's behalf.
{
"Effect": "Allow",
"Action": ["ec2:StartInstances", "ec2:StopInstances"],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/team": "${aws:PrincipalTag/team}",
"aws:PrincipalOrgID": "o-example"
},
"Bool": { "aws:MultiFactorAuthPresent": "true" },
"IpAddressIfExists": { "aws:SourceIp": "203.0.113.0/24" }
}
}ABAC has one hard prerequisite: tag integrity. If a principal can write the team tag on a resource, it can grant itself access, so tag-write becomes as sensitive as policy-write and must be denied outside a controlled path. The second trap is absence semantics: IfExists and Null-family operators pass when the key is missing, which silently disables a control when reached for by reflex. For the AWS condition-key catalogue, see AWS IAM conditions.
Guardrails - permission boundaries and org-level ceilings
Neither mechanism grants anything; both only subtract, which is why a guardrail survives someone else editing a policy you do not control.
A permission boundary attaches to a principal and caps what it can ever hold: effective permission is the boundary intersected with the identity policy. Its purpose is safe delegation - a team lead may create roles for their own services without being able to create one more powerful than the boundary. It only holds if the delegating policy also requires, by condition, that every role they create carries the boundary; otherwise they create an unbounded role and assume it.
Organization-level ceilings sit above the account admin over a whole subtree. AWS uses Service Control Policies on organizational units. GCP splits the job between IAM deny policies (a principal may not use a permission) and Organization Policy constraints (resource-configuration rules such as disabling service-account key creation). Azure applies Azure Policy at management-group scope. A sane starting set: audit logging cannot be disabled nor its destination deleted, regions are allow-listed, long-lived keys cannot be created, storage cannot be made public. Test in a sandbox subtree first: a broad deny routinely breaks a service-linked role nobody knew existed. Folder structuring is covered in cloud landing zones.
Short-lived credentials, role assumption, and federation
A long-lived static access key is the highest-value artifact in a cloud estate: it never expires, it is trivially copied into a repository, and the audit trail records the key rather than the person. The replacement is token exchange - every credential becomes a session with an audience, an expiry and a visible issuance event. Vendors cap session lifetime, commonly an hour by default.
Three flavours cover nearly everything. In-cloud workload identity: the platform hands the process a token through a local metadata endpoint based on where the code runs. Cross-account assumption: the target role's trust policy names the calling account, plus an external identifier for third parties - the classic confused deputy defence. External federation: SAML or OIDC from an identity provider or CI platform.
# CI job gets a cloud credential with no stored secret
1. CI mints an OIDC id token:
iss = https://token.ci.example
sub = repo:acme/payments:ref:refs/heads/main
aud = cloud-sts
2. AssumeRoleWithWebIdentity(role_arn, id_token)
3. STS checks JWKS signature, aud, expiry, then matches
StringEquals sub = repo:acme/payments:ref:refs/heads/main
4. short-TTL session credential returned
bug: pinning iss/aud but leaving sub open lets ANY repo
on that platform assume the role.Two details decide whether this is security or theatre: the trust policy must constrain the subject and not just the issuer - per repository, per branch, per environment - and session names or tags must propagate so audit shows the human behind a machine session. Lock the metadata endpoint down as well, and treat any workload SSRF as credential theft.
From access logs to least privilege
Least privilege is derived from evidence, not authored from documentation. Instrument first: enable the audit trail in every account, ship it somewhere no workload principal can delete, and turn on the data-plane events - object reads, secret fetches - that are off by default on sensitive stores.
Observe with the broad policy in force over a window spanning a release and a month-end close, recording every principal, action and resource triple actually exercised. Generate a candidate from that set - AWS Access Analyzer builds one from CloudTrail, GCP's role recommender proposes cuts from roughly ninety days of usage, Azure has an equivalent permissions report - then widen it deliberately for paths the window missed: failover drills, quarterly jobs, incident response. Shadow before enforcing: keep the broad policy but alert whenever a call falls outside the candidate. That is the step teams skip and the one that makes the cutover non-breaking. Then enforce and keep looping, with unused-access findings as the steady-state input. One caveat: logs record only what was permitted, so review denied-call metrics alongside.
The classic failure modes
Compounded wildcards. A wildcard action with a narrow resource is often fine; a narrow action with a wildcard resource is often fine; both together is an administrator. And any policy containing IAM-write wildcards is administrator whatever else it says, because it can rewrite itself.
Escalation through IAM-write permissions. The canonical path is being allowed to hand a powerful identity to something you control: iam:PassRole with a compute-launch permission on AWS, iam.serviceAccounts.actAs with a deploy permission on GCP, Microsoft.Authorization/roleAssignments/write on Azure. Policy mutation - new policy versions, attaching managed policies, rewriting trust documents - is the same class. Treat every IAM-write permission as full admin unless it is conditioned down to specific target identities.
Wide-open resource policies. A resource policy naming an anonymous or wildcard principal is the public internet, and a wildcard principal with a condition that narrows nothing is the same with better optics. Run the public-access analyzers on a schedule.
Unconstrained trust and untested break-glass. A CI trust policy that validates the issuer but leaves the subject open lets any project on that platform assume the role. And an emergency path never exercised does not work, while one exercised routinely is an admin account with a dramatic name.
End-to-end flow
End-to-end: an engineer needs to debug a production issue. She authenticates via SSO, is prompted for MFA. She assumes the prod-debug role, which is bounded to read-only actions. Her session has a 1-hour cap. She queries the necessary logs. Every API call is written to CloudTrail. A workload service assumes a role via STS with no long-lived credentials to read a secret from Secrets Manager; the role's policy allows only that specific secret ARN. An SCP prevents any account from disabling CloudTrail. Quarterly, the org runs an access review; drift is flagged and corrected. If a compromise happens, audit trail replay identifies scope and enables surgical remediation.
Cloud IAM is three operators stacked: a union of allows, a veto from any explicit deny, and an intersection with every ceiling above it. Federate humans so no person holds a cloud password, give workloads short-lived tokens so no key is stored, cap delegated admins with boundaries and whole accounts with organization guardrails, and derive the real permission set from audit logs rather than documentation. The failure mode is never a dramatic breach of the model - it is drift.