For anything running inside AWS — EC2, Lambda, ECS, CodeBuild — the answer is a role, full stop: roles issue short-lived credentials automatically and there is nothing to leak, rotate, or forget. For humans, the modern answer is IAM Identity Center sessions, not IAM users. IAM users with long-lived access keys are the legacy option, defensible only in a shrinking set of edge cases, and every audit framework now treats each one as a finding to justify.
What you are deciding
The decision is really about credential lifetime. An IAM user carries a password and up to two access keys that live until someone deletes them — often years, per the credential report on almost any mature account. A role has no credentials of its own; a trusted principal assumes it and receives STS credentials that expire in an hour by default (up to the role’s configured maximum). Everything else follows from that difference: keys end up in Git history, laptops, and CI logs, while role sessions expire before most leaks are exploitable, and every session in CloudTrail carries a session name that says who assumed it.
So the practical fork looks like this. Workloads inside AWS: always a role — instance profiles on EC2 (the full pattern, including IMDSv2 enforcement, is in our instance profile guide), execution roles on Lambda and ECS. Workloads outside AWS: still usually a role, reached through a federation door — GitHub Actions and GitLab authenticate with OIDC to assume a role, on-premises servers can use IAM Roles Anywhere with X.509 certificates. Humans: Identity Center or your existing IdP federated into roles. What remains for IAM users with keys is genuinely small: a third-party tool that only accepts static AWS keys, legacy SMTP credentials for SES in older setups, and transitional accounts mid-migration. The IAM documentation itself now steers every one of these audiences away from long-term credentials.
What to check first
- Count your IAM users.
aws iam list-users— every entry needs a story: human (should be in SSO), machine (should be a role), or documented exception. - Count access keys and their ages. Keys older than 90 days with active use mark the places migration will hurt; keys unused for 90 days can simply be removed.
- Check console access. Users with passwords are humans authenticating the legacy way.
- Look for shared users. A user named
deployoradminused by several people means CloudTrail cannot attribute actions — the worst of both worlds.
How to verify
# Every user, every key, last-used — the whole picture
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d > report.csv
# Key age and status per user
aws iam list-access-keys --user-name deploy-user
# When was a key last used, and by which service?
aws iam get-access-key-last-used --access-key-id AKIAIOSFODNN7EXAMPLE
Console path: IAM > Users, sort by “Last activity”, and IAM > Credential report for the CSV. A user whose password_last_used and both key last-used columns are all blank or ancient is a deletion candidate, not a debate.
The approach
- Freeze the pattern first. Decide, and write down, that new workloads get roles and new humans get SSO. Stopping new users costs nothing and keeps the cleanup finite.
- Move in-AWS workloads to roles. Attach instance profiles, task roles, and execution roles; delete the keys after last-used confirms silence. This is the highest-value, lowest-friction batch.
- Move CI/CD to OIDC federation. GitHub Actions, GitLab, and Bitbucket all assume roles via OpenID Connect; the stored AWS secret disappears from the CI vault entirely.
- Move humans to Identity Center. Permission sets replace per-user policies, and MFA is enforced at the identity source instead of by policy contortions.
- Document the survivors. Each remaining user gets an owner, a reason, a rotation schedule, and a tightly scoped policy — a named exception, not a leftover.
When this is the wrong move
Do not delete a key the moment last-used shows activity you cannot place — that activity is a production dependency you have not found yet; deactivate, wait, and listen instead. And a same-day forced migration of a critical third-party integration that only supports static keys trades a theoretical risk for a real outage: schedule it with the vendor, and rotate the key on a calendar until then.
Common mistakes
- Creating an IAM user “just for this script” running on EC2 — the instance profile was always the answer.
- Sharing one user between team members, which destroys attribution in CloudTrail.
- Treating rotation as the fix for keys that should not exist — rotation is the fallback for the exceptions, not the strategy.
- Forgetting the root user in the audit: root should have MFA, no access keys, and near-zero sign-ins.
Keeping it under control
- Alert on
CreateAccessKeyandCreateUserevents via CloudTrail so new long-lived credentials are a conversation, not a surprise. - Run the credential report monthly; trend the user count toward your documented exception list.
- Set an SCP or standing policy denying key creation for consoles-only human users where feasible.
- Re-check vendor tools yearly — integrations that once demanded static keys frequently gain role support.
The role-vs-user question is one of the few in AWS with a clean answer, which makes lingering key sprawl an ownership problem rather than a technical one. If nobody on your team truly owns the AWS environment end to end, that is the root of most of these problems — Stack Harbor runs AWS accounts daily under AWS Operations, and you can book an AWS review.