Skip to content

IAM roles vs users: when each is right (and why access keys should be rare)

IAM roles vs users is settled for workloads — roles win. The real decision is what to do with humans and the handful of cases where a user with access keys still earns its place.

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 deploy or admin used 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

  1. 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.
  2. 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.
  3. 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.
  4. Move humans to Identity Center. Permission sets replace per-user policies, and MFA is enforced at the identity source instead of by policy contortions.
  5. 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 CreateAccessKey and CreateUser events 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.