IAM Users, Roles, Policies, and Evaluation Logic
AWS Identity and Access Management decides who can call each AWS API and what the call is allowed to affect. In this lesson, the outcome is practical: you should be able to look at a principal, its policies, the target resource, and any guardrails, then predict whether AWS returns success or AccessDenied. That skill is central to AWS cloud engineering because almost every design choice eventually becomes an authorization question: can the deployment role create this resource, can the application read that secret, and can an operator recover without receiving administrator access?
Purpose and Outcome
IAM has four core building blocks. A user is a long-lived IAM identity, usually reserved for exceptional human or service cases that cannot use federation. A role is an assumable identity with temporary credentials from AWS Security Token Service. A policy is a JSON document containing permission statements. The evaluation logic is the deterministic process AWS uses to combine identity policies, resource policies, permission boundaries, session policies, service control policies, resource control policies where used, and explicit denies.
The preferred AWS pattern is to minimize IAM users, federate humans through an identity provider, and let workloads assume roles. Users and roles are both principals, but their operating model is different. Users own long-lived credentials until rotated or removed. Roles have trust policies that say who may assume them, and permissions policies that say what temporary role sessions may do after assumption.
How IAM Evaluation Works
An AWS request carries a principal, an action, a resource, request context, and sometimes session tags or source identity. IAM first authenticates the requester. It then gathers policies that apply to the request. If any applicable policy has an explicit Deny matching the action, resource, and conditions, the request is denied. If there is no explicit deny, AWS looks for an applicable Allow. With identity-based and resource-based policies, an allow can grant access only when it is not limited away by guardrails such as permissions boundaries, session policies, service control policies, or resource control policies. If no allow remains, the default result is an implicit deny.
Think of identity policies and resource policies as grant sources, and boundaries or organization policies as maximum-permission filters. A role can have a policy allowing s3:DeleteObject, but if its permissions boundary allows only s3:GetObject, deletion is still denied. Similarly, a member account administrator cannot bypass an AWS Organizations service control policy that blocks an action. Explicit deny is stronger than every allow because it is designed for enforceable guardrails.
Policy Anatomy
A policy document has a Version and a Statement list. Each statement commonly includes Effect, Action or NotAction, Resource or NotResource, and optional Condition. Identity policies normally omit Principal because the policy is attached to the principal. Resource policies include Principal because the resource must name who is allowed or denied. Trust policies are resource-based policies attached to roles; their resource is the role, and their main action is usually sts:AssumeRole.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOneBucketPrefix",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::example-reports-bucket/team-a/*"
}
]
}
This policy allows object reads only under one S3 prefix. It does not allow listing the bucket, writing objects, reading another prefix, or reading a different bucket. If an application also needs to discover keys, it usually needs s3:ListBucket on the bucket ARN with a condition such as s3:prefix; object ARNs and bucket ARNs are separate resources.
Example 1: Identity Policy for Read-Only S3 Access
Start with an application role that must read generated reports from one prefix. The identity policy grants only object read. A call to GetObject for s3://example-reports-bucket/team-a/summary.csv is allowed if no boundary or organization policy blocks it. A call to PutObject on the same key is implicitly denied because no statement allows it. A call to GetObject for team-b/summary.csv is also implicitly denied because the resource does not match.
set -euo pipefail
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111122223333:role/report-reader \
--action-names s3:GetObject s3:PutObject \
--resource-arns arn:aws:s3:::example-reports-bucket/team-a/summary.csv \
--output json
The deterministic result should show allowed for s3:GetObject and implicitDeny for s3:PutObject, assuming the role exists and has only the policy shown. Policy simulation is useful because it separates authorization reasoning from application code and network behavior.
Example 2: Role Trust Versus Role Permissions
A role has two different policy questions. The trust policy controls who may assume the role. The permissions policy controls what a successful session may do. Granting sts:AssumeRole in the caller account is not enough if the target role trust policy does not trust that caller.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111122223333:role/ci-deployer"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {"sts:ExternalId": "course-lab-deploy"}
}
}
]
}
With this trust policy, ci-deployer can assume the role only when it supplies the expected external ID. Another role in the same account is denied. The same role without the external ID is denied. If assumption succeeds, AWS returns temporary credentials with an expiration time; those credentials then receive only the permissions attached to the assumed role, optionally reduced by a session policy.
Example 3: Explicit Deny as a Guardrail
Explicit deny is how teams create permissions that application teams cannot accidentally widen. The following statement denies deletion for all objects in a bucket unless the principal carries an approved break-glass tag in the request context.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyDeleteWithoutBreakGlass",
"Effect": "Deny",
"Action": "s3:DeleteObject",
"Resource": "arn:aws:s3:::example-reports-bucket/*",
"Condition": {
"StringNotEquals": {"aws:PrincipalTag/breakglass": "approved"}
}
}
]
}
If a principal has an identity policy allowing s3:DeleteObject but lacks the tag, deletion is denied. If it has the tag and an allow, deletion can proceed. If it has the tag but no allow, deletion is still implicitly denied. The deny statement does not grant anything; it only removes a dangerous path unless the condition is satisfied.
Design Choices and Trade-Offs
Use roles for workloads because temporary credentials reduce the value of credential theft and fit compute services such as EC2, Lambda, ECS, EKS, and CodeBuild. Use IAM users sparingly for cases that cannot federate or assume roles, then enforce MFA where applicable, rotate access keys, and monitor unused credentials. Prefer customer-managed policies for reusable permission sets that your team owns, but use inline policies when permissions must be tightly coupled to exactly one principal and should disappear with it.
Resource policies are better when the resource owner should control access, such as an S3 bucket accepting a cross-account role or a KMS key naming key administrators and users. Identity policies are better when the identity owner controls what a role can do across services. Conditions are powerful but easy to overcomplicate. A condition on aws:SourceVpce, aws:PrincipalOrgID, tags, MFA, source IP, or encryption headers can express important controls, but every condition key has service-specific behavior that must be tested against real requests.
Failure Modes and Troubleshooting
Symptom: an application receives AccessDenied for an action that appears allowed. Cause: a permissions boundary, session policy, service control policy, or explicit deny is filtering the allow. Diagnostics: inspect the principal policies, role boundary, session policy passed to AssumeRole, relevant resource policy, and AWS Organizations policies. Run aws iam simulate-principal-policy when the action supports simulation. Correction: add the narrow missing allow or adjust the guardrail only if the business rule permits it.
Symptom: AssumeRole fails before the application calls the target service. Cause: the caller lacks permission to call sts:AssumeRole, or the target trust policy does not trust the caller, external ID, source ARN, or web identity claim. Diagnostics: compare the caller ARN from aws sts get-caller-identity with the role trust policy and CloudTrail failure event. Correction: update the caller permission and target trust policy as a pair.
Symptom: S3 list works but object reads fail, or object reads work but list fails. Cause: bucket-level actions and object-level actions use different ARNs. Diagnostics: check whether s3:ListBucket uses arn:aws:s3:::bucket and s3:GetObject uses arn:aws:s3:::bucket/key. Correction: add the exact missing statement with prefix conditions instead of broad bucket access.
Security, Reliability, and Performance Implications
IAM is a security control, but it also affects reliability. Overbroad permissions make incidents larger because a compromised role can move farther. Overly narrow or untested permissions cause deployment failures and recovery delays. Temporary credentials add a small operational dependency on STS and credential refresh behavior, so workloads must use AWS SDK credential providers instead of caching credentials indefinitely. Large numbers of managed policy attachments and complex conditions can make human review difficult, even though IAM evaluation itself is a managed AWS control plane function.
Hands-On Lab: Predict and Verify an Access Decision
Prerequisites: an AWS sandbox account, AWS CLI configured for a principal allowed to create IAM roles and policies, and a uniquely named S3 bucket for testing. Do not use a production account. Replace account IDs and bucket names before running commands.
- Create a role named
course-report-readerwith a trust policy that allows your current administrative lab principal to assume it. - Attach a policy that allows
s3:GetObjectonly onarn:aws:s3:::YOUR-BUCKET/team-a/*. - Upload two test objects:
team-a/summary.txtandteam-b/summary.txt. - Assume the role and export the returned temporary credentials in a shell dedicated to the lab.
- Run
aws s3 cp s3://YOUR-BUCKET/team-a/summary.txt -. Verification: the file contents print to standard output. - Run
aws s3 cp s3://YOUR-BUCKET/team-b/summary.txt -. Verification: AWS returnsAccessDenied. - Run a write attempt to
team-a/new.txt. Verification: AWS returnsAccessDeniedbecauses3:PutObjectwas never allowed.
set -euo pipefail
aws sts get-caller-identity
aws s3 cp s3://YOUR-BUCKET/team-a/summary.txt -
aws s3 cp s3://YOUR-BUCKET/team-b/summary.txt - || true
aws s3 cp ./local-test.txt s3://YOUR-BUCKET/team-a/new.txt || true
Cleanup: delete the uploaded test objects, detach and delete the lab policy, delete the role, and remove any temporary credential exports from your shell. Verification after cleanup is that aws iam get-role --role-name course-report-reader returns no such entity.
Assessment Exercises
- A role has an allow for
dynamodb:PutItem, but its permissions boundary allows onlydynamodb:GetItem. Predict the result ofPutItemand explain which policy type decides the outcome. - A bucket policy allows a cross-account role to read objects, but the role identity policy has no S3 permissions. Explain when the resource policy alone can be sufficient and what other guardrails might still block the request.
- Write the smallest S3 permission set for listing only the
team-a/prefix and reading objects only under that prefix. - During a failed deployment, CloudTrail shows
AssumeRoledenied. List the two policy documents you would inspect first and why. - Design an explicit deny that prevents deleting KMS keys except for a named break-glass role, and describe how you would test both allowed and denied paths.
Summary
IAM authorization is not a guess about one policy attached to one identity. It is the combined result of principal type, trust, identity grants, resource grants, conditions, boundaries, session limits, organization guardrails, and explicit denies. Good AWS designs use roles and temporary credentials by default, express least privilege with exact actions and ARNs, test expected denials as carefully as expected allows, and keep break-glass paths narrow enough to be useful during recovery without becoming normal administration.
