AWS Identity and Access Management (IAM) is the gatekeeper for every API call in your account. When IAM is wrong, deployments fail with opaque "AccessDenied" errors—or worse, something public that should not be.
Developers do not need to memorize every IAM edge case. You need a clear model of who is calling AWS, what they are allowed to do, and how permissions combine.
The four objects you will touch daily
1. IAM users (mostly for humans, increasingly rare)
An IAM user is a long-lived identity with credentials. AWS now recommends federated login (SSO) instead of creating users for each engineer. You still see IAM users for legacy CI bots—prefer roles where possible.
2. IAM roles (machines and temporary access)
A role is an identity assumed temporarily. EC2 instances, Lambda functions, GitHub Actions, and ECS tasks should use roles—not access keys baked into environment variables.
Flow: service calls sts:AssumeRole → receives short-lived credentials → calls AWS APIs.
3. Policies (the actual permission document)
Policies are JSON documents with Allow and Deny statements. Example allowing read-only S3 access to one bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-app-assets",
"arn:aws:s3:::my-app-assets/*"
]
}
]
}
Attach policies to users, groups, or roles. AWS also ships managed policies (e.g., AmazonS3ReadOnlyAccess)—convenient but often too broad for production.
4. Groups (organize humans)
Groups bundle policies for teams ("backend", "data"). Users inherit group permissions. Roles do not belong in groups.
How AWS decides yes or no
When a request arrives, IAM evaluates:
- Explicit Deny always wins.
- Allow statements across all applicable policies are unioned.
- Resource-based policies (S3 bucket policy, Lambda permission) can also grant access cross-account.
- Service Control Policies (SCPs) in AWS Organizations can cap what accounts allow—even if an admin attaches
AdministratorAccess.
If you are debugging Deny, check SCPs, permission boundaries, and session policies—not just the role you think you edited.
Roles your app actually needs
| Component | Typical role pattern |
|---|---|
| EC2 / ECS task | Instance/task role with least privilege for S3, DynamoDB, SQS |
| Lambda | Execution role per function or shared by domain |
| CI/CD (GitHub Actions) | OIDC trust → role with deploy permissions scoped to one account/region |
| Cross-account access | Role in target account trusted by source account role |
Trust policy defines who can assume the role. Permission policy defines what they can do after assuming it. Mixing them up is a common mistake.
Example trust for GitHub OIDC (simplified):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"
}
}
}
]
}
Least privilege without drowning in JSON
Start broad in dev; tighten in staging/production using CloudTrail and IAM Access Analyzer:
- Deploy with a permissive role in a sandbox.
- Run real workloads; collect
AccessDeniedand successful API calls. - Replace
"Action": "*"with specific actions (s3:PutObject,sqs:SendMessage). - Scope
ResourceARNs instead of"*"where possible.
Use permission boundaries for developer roles: they can create IAM policies only within a cap—preventing accidental admin escalation.
Debugging AccessDenied in minutes
- Open CloudTrail event for the failed call—note
eventName,resourceArn,userIdentity. - Paste into IAM Policy Simulator with the role/user attached policies.
- Check resource-based policies (S3 bucket policy denying public access is common).
- Confirm region and account ID in ARNs—copy-paste errors happen constantly.
For assumed roles, remember the session name in logs to trace which pod or workflow failed.
Anti-patterns to retire
- Long-lived access keys in repos → rotate to OIDC roles.
- Shared "deploy" user used by every pipeline → separate roles per environment.
- AdministratorAccess on CI because it "fixes" deploys → scoped deploy role with
cloudformation:*on one stack prefix. - Wildcard resources on destructive actions (
s3:DeleteObjecton"*").
IAM and application code
SDKs automatically pick up credentials from:
- Environment variables (
AWS_ACCESS_KEY_ID—avoid in prod) - Instance/task role via metadata service
~/.aws/credentialslocally
On EKS, use IRSA (IAM Roles for Service Accounts) to map Kubernetes service accounts to IAM roles—far better than node-wide permissions.
Local dev tip: use AWS SSO profiles instead of static keys; aws sso login refreshes short-lived tokens.
Cross-account patterns teams actually use
Central logging account: Application accounts assume a role in the logging account to write CloudWatch logs or S3 audit buckets. The trust policy lists only known source account IDs.
Shared services account: Platform team hosts ECR, Route53 zones, or Terraform state. Workload accounts receive sts:AssumeRole into a role that can pull images or update DNS—never the reverse.
Break-glass admin: A highly audited role with MFA required, no standing access, and CloudTrail alerts on assume-role events. Document the runbook; use it rarely.
When drawing these on a whiteboard, label trust direction (who assumes whom) separately from API permissions (what happens after assume).
IAM policy conditions worth knowing
Conditions narrow broad statements without duplicating policies:
aws:SourceIp– restrict admin actions to corporate VPN CIDRs.aws:MultiFactorAuthPresent– require MFA for sensitive operations.StringLikeonaws:userid– limit which roles in an account can pass a role to Lambda.s3:prefix– allow listing only under a tenant prefix in a shared bucket.
Example tightening S3 upload:
{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::uploads-prod/*",
"Condition": {
"StringLike": {
"s3:prefix": ["tenant-${aws:PrincipalTag/TenantId}/*"]
}
}
}
Tag sessions at assume-role time so ABAC-style rules can apply consistently.
FAQ
What's the difference between IAM and Cognito?
IAM controls access to AWS APIs. Cognito handles application user sign-up/sign-in (often for your product's customers). They solve different problems.
Do I need IAM users for each microservice?
No—each service should assume a role. Users are for humans or exceptional legacy integrations.
Why does my Lambda have permissions but still fails on KMS or VPC?
Additional policies may be required (kms:Decrypt, ENI management). The error message usually names the missing action.
How often should I audit IAM?
Quarterly at minimum; automate reports for unused roles and access keys older than 90 days.
What is PassRole and why does Lambda creation fail without it?
Creating Lambda requires passing an execution role to the service. The creator needs iam:PassRole on that role and often a condition that the role is only passed to lambda.amazonaws.com.
Can I test IAM changes safely?
Use a sandbox account, IAM Access Analyzer policy validation, and the policy simulator before promoting JSON to production roles.
Starter checklist for a new microservice
Before first deploy, confirm:
- Task/function role created with trust for ECS/Lambda/EKS service
- Policies scoped to specific ARNs (S3 bucket, SQS queue, DynamoDB table)
- No access keys in environment variables or git
- CloudTrail enabled in the account
- Break-glass role documented but not used day-to-day
- CI role uses OIDC, not static user keys
Thirty minutes of IAM design upfront prevents weekend firefighting when production suddenly cannot read a secret or publish to a queue.
IAM clicks into place once you separate identity (who), trust (who can become that identity), and authorization (what they can touch). Nail those three and AWS stops feeling like a black box of permission errors.
Comments
Loading comments…