AWS IAM Best Practices
Practical IAM hardening. Replacing IAM users with Identity Center, least privilege from real usage data, permissions boundaries, MFA, and what to audit regularly.
IAM failures are asymmetric. An over-permissive policy works perfectly until it is abused; an under-permissive one fails immediately and gets fixed. That asymmetry is why permissions drift outward over time unless something deliberately pushes back.
These are ordered by impact, not by how often they appear on checklists.
1. Stop creating IAM users for people
This is the highest-impact change available and the one most often skipped.
An IAM user holds permanent credentials, a console password and often access keys, that remain valid until someone deletes them. They get committed to repositories, pasted into scripts, kept on former employees' laptops, and forgotten.
Use IAM Identity Center (formerly AWS SSO), or federate from your existing identity provider. People then sign in with corporate credentials and receive a temporary role session. There are no access keys to leak, offboarding happens in one place, and access across multiple accounts is managed centrally.
# Find the users you still have
aws iam list-users --query 'Users[].[UserName,CreateDate,PasswordLastUsed]' --output table
Keep IAM users only where a long-lived credential is genuinely unavoidable, and treat each as a liability with a named owner.
2. Remove long-lived access keys from CI
The second-largest exposure. A pipeline holding AWS_SECRET_ACCESS_KEY has a credential that works from anywhere, forever, until rotated.
OIDC federation removes it entirely. Register your CI provider as an OIDC identity provider, create a role whose trust policy accepts its tokens, and condition on the specific repository and branch:
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:acme/payments:ref:refs/heads/main"
}
}
That sub condition is not optional. Without it, any repository on GitHub can assume your role. It is a genuine and not-uncommon misconfiguration.
Find keys that still exist:
aws iam generate-credential-report
aws iam get-credential-report --query Content --output text | base64 -d | column -t -s,
The report shows every user, whether MFA is enabled, and when each key was last used. Keys unused for 90 days should be deleted, not rotated.
3. Lock the root user away
The root user cannot be restricted by IAM policy or by a service control policy. It is the one identity that can do anything.
- Enable hardware MFA on it.
- Delete its access keys. There is no legitimate reason for root to have programmatic credentials.
- Use it only for the handful of tasks that genuinely require it. Closing the account, changing the support plan, some billing operations.
- Set up alerting on root sign-in via CloudTrail and EventBridge.
4. Get to least privilege from data, not guesswork
Starting with AdministratorAccess "for now" never gets narrowed. But hand-writing a minimal policy is slow and usually wrong in both directions.
Use what AWS already records:
# What has this role actually used, and when?
aws iam generate-service-last-accessed-details --arn arn:aws:iam::111122223333:role/DeployRole
IAM Access Analyzer policy generation goes further. It reviews CloudTrail history for a role and generates a policy containing only the actions genuinely used. That is a far better starting point than a guess, and it turns least privilege from a research project into a review task.
Access Advisor in the console shows services a principal has not touched in months. Those are the safest permissions to remove first.
Apply reductions in a non-production account and watch CloudTrail for denials before promoting.
5. Prefer customer-managed policies
| Type | Problem |
|---|---|
| AWS managed | Almost always broader than needed: AmazonS3FullAccess on a role that reads one prefix |
| Inline | Invisible in policy listings, unreusable, no version history |
| Customer managed | Reusable, versioned, auditable (use these) |
Inline policies are the ones that surprise people during an audit, because list-attached-role-policies does not show them:
aws iam list-attached-role-policies --role-name DeployRole # managed
aws iam list-role-policies --role-name DeployRole # inline. Run both
Customer-managed policies keep up to five versions, so a bad change rolls back in one command.
6. Use conditions rather than enumerating resources
Conditions make least privilege practical at scale:
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::acme-*/*",
"Condition": {
"StringEquals": { "aws:PrincipalTag/team": "${aws:ResourceTag/team}" },
"Bool": { "aws:SecureTransport": "true" }
}
}
Attribute-based access control, matching a principal's tag against a resource's tag, scales where enumerating every ARN does not. Other high-value conditions require MFA for sensitive actions, restrict access to a VPC endpoint, or limit a role to specific regions.
7. Permissions boundaries for safe delegation
A boundary caps the maximum permissions a principal can have. Effective permissions are the intersection of its policies and the boundary; a boundary never grants anything.
The main use is letting teams create their own roles without being able to escalate:
{
"Effect": "Allow",
"Action": "iam:CreateRole",
"Resource": "*",
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary": "arn:aws:iam::111122223333:policy/TeamBoundary"
}
}
}
Note the flip side: a boundary is a silent cause of denials. A principal with AdministratorAccess and a restrictive boundary is not an administrator, and nothing in the error says so.
8. Service control policies for organisation-wide guardrails
SCPs apply across an AWS Organization and cannot be overridden by any account-level policy, including root. They set ceilings rather than granting anything.
Common guardrails: deny the ability to leave the organisation, deny disabling CloudTrail or GuardDuty, deny regions you do not operate in, deny deleting specific backup resources.
9. Audit on a schedule
| Check | Command or tool |
|---|---|
| Unused users, keys and MFA status | aws iam generate-credential-report |
| Permissions never exercised | Access Advisor, generate-service-last-accessed-details |
| Resources shared externally | IAM Access Analyzer |
| Root usage | CloudTrail with an EventBridge alert |
| Overall posture | AWS Security Hub |
Access Analyzer is worth enabling regardless of whether you suspect a problem. It continuously reports which resources are accessible from outside the account, which is a question most teams cannot otherwise answer.
Quick reference
| Task | Command |
|---|---|
| Who am I | aws sts get-caller-identity |
| Credential report | aws iam generate-credential-report then get-credential-report |
| Test a permission | aws iam simulate-principal-policy --policy-source-arn <arn> --action-names <action> |
| Check as another principal | aws iam simulate-principal-policy --policy-source-arn <role-arn> ... |
| Unused permissions | aws iam generate-service-last-accessed-details --arn <arn> |
| Managed policies on a role | aws iam list-attached-role-policies --role-name <n> |
| Inline policies on a role | aws iam list-role-policies --role-name <n> |
| Roles in the account | aws iam list-roles --query 'Roles[].RoleName' |
Key takeaways
- Stop creating IAM users for people. Identity Center or federation removes permanent credentials entirely.
- OIDC removes long-lived keys from CI: and the trust policy must condition on
sub, or any repository can assume the role. - Root has no access keys, hardware MFA, and an alert on sign-in.
- Generate least-privilege policies from CloudTrail with Access Analyzer rather than guessing.
- Check inline policies separately: they do not appear in the attached-policies listing.
- Conditions and tag-based access scale where enumerating ARNs does not.
- Permissions boundaries enable safe delegation and are a silent cause of denials.
- Enable Access Analyzer to see what is reachable from outside the account.
Check your understanding
An OIDC role anyone can assume
A GitHub Actions OIDC trust policy conditions only on the aud claim. What is the risk?
A permission with no visible source
An audit lists a role's attached policies and finds nothing granting S3 access, yet the role can read buckets. What was missed?
Frequently Asked Questions
Should I still create IAM users?
Not for people. Current AWS guidance is to use IAM Identity Center or federate from an existing identity provider, so humans sign in with corporate credentials and receive a temporary role session with no permanent access keys to leak. Offboarding then happens in one place rather than across every account. Reserve IAM users for the narrow cases where a long-lived credential is genuinely unavoidable, and give each one a named owner and a review date.
How do I remove AWS access keys from my CI/CD pipeline?
Use OIDC federation. Register your CI provider as an OIDC identity provider in AWS, create a role whose trust policy accepts its tokens, and condition on the specific repository and branch. The pipeline exchanges a short-lived signed token for temporary credentials at run time, so nothing long-lived is stored and there is nothing to rotate. The critical detail is conditioning on the sub claim, without it, any repository on that provider can assume your role.
How do I actually achieve least privilege?
Use the data AWS already collects rather than guessing. IAM Access Analyzer generates a policy from a role's real CloudTrail activity, giving a starting point containing only actions genuinely used. Access Advisor shows which services a principal has not touched in months, which are the safest permissions to remove first. Apply reductions in a non-production account and monitor CloudTrail for denials before promoting. Starting broad with the intention of narrowing later almost never results in narrowing.
What is a permissions boundary and when should I use one?
A boundary caps the maximum permissions a principal can have. Effective permissions are the intersection of its policies and the boundary, and the boundary never grants anything itself. The main use is safe delegation: allowing a team to create IAM roles for their own workloads while requiring that every role they create carries your boundary. Be aware it is also a silent cause of denials, since a principal with AdministratorAccess and a restrictive boundary is not an administrator and the error does not say why.
How should I secure the AWS root user?
Enable hardware MFA, delete any access keys it has, and use it only for the small number of tasks that genuinely require root. Closing the account, changing the support plan, certain billing operations. Set up a CloudTrail and EventBridge alert on root sign-in so any use is noticed immediately. Root is the one identity that cannot be restricted by IAM policies or service control policies, which is precisely why it should be locked away rather than managed.
What should I audit regularly in IAM?
Run a credential report to find users without MFA and access keys unused for 90 days, which should be deleted rather than rotated. Use Access Advisor to find permissions never exercised. Enable IAM Access Analyzer, which continuously reports which resources are reachable from outside the account. A question most teams cannot otherwise answer. Alert on root sign-in. And check both attached and inline policies, since inline ones do not appear in the attached-policies listing and are easy to miss entirely.