Least privilege in AWS without guessing: the Access Analyzer workflow

Nobody writes a minimal IAM policy from a blank page. You harvest it from what the role actually did. Here is the loop we run to shrink permissions without breaking production.

Ask any team why a role has PowerUserAccess and the answer is always some version of the same story: it was a deadline, someone attached the managed policy to unblock a deploy, and nothing broke, so nobody went back. Two years later it is a thousand permissions wide and the person who attached it has left.

Least privilege fails when you treat it as an authoring problem. Nobody can write a minimal policy for an unfamiliar service from memory. It is a harvesting problem: let the role run, watch what it uses, and write down exactly that.

The loop

1. Rank the roles by damage, not by count. Pull the roles in your production accounts and sort by two things: whether the trust policy allows a human or an external principal, and whether the role can write to IAM, KMS, S3 or the network. A read-only role used by a hundred people is less interesting than a CI role that can call iam:PassRole with a wildcard.

2. Read the access advisor data. For each role, aws iam get-service-last-accessed-details gives you the services it has touched and when. Services untouched in 180 days come out of the policy. This one step removes more surface than anything else you will do this quarter, and it is almost risk-free because you can see the last-used timestamp before you cut.

3. Generate a policy from CloudTrail. IAM Access Analyzer's policy generation reads the trail for a role over a time window (up to 90 days) and emits a policy scoped to the actions it actually called. Two caveats we have learned the hard way:

  • The window has to cover your slowest cycle. If a role only runs a quarterly job, a 90-day window may miss it. Note those jobs before you cut and add their actions by hand.
  • Generated policies are action-scoped but often resource-wildcarded. Tightening resources is manual work, and it is where the real value is: s3:GetObject on * and on one bucket are very different permissions.

4. Deploy to staging with the old policy still attached. Attach the new tight policy as an additional policy and leave the broad one in place for a week while you watch for AccessDenied in CloudTrail. Then remove the broad one. Denials are logged even when the request succeeds through another policy, so you get a free dry run.

5. Lock the perimeter with external access findings. Access Analyzer's external access findings tell you which roles, buckets, KMS keys, Lambda functions and SQS queues are reachable from outside your organisation. Set the zone of trust to the organisation, not the account, and triage every finding to either "archived with a written reason" or "fixed". An unreviewed finding list is worse than none, because it teaches the team to ignore the console.

The four findings that repeat

Across assessments, the same four show up in almost every account.

  • iam:PassRole with Resource: "*". Any principal holding it can hand a service a far more privileged role than its own. Scope it to the exact role ARNs, and add a iam:PassedToService condition.
  • Wildcard trust policies on cross-account roles. A role trusting an entire external account rather than a specific role in it, with no sts:ExternalId. This is the confused-deputy setup vendors keep asking for.
  • Long-lived access keys on IAM users. Usually for CI. Replace with OIDC federation from your CI provider, which is an afternoon of work and deletes the whole category.
  • Inline policies nobody can diff. Move them to managed policies in Terraform so changes show up in a pull request.

What to enforce afterwards

Harvesting shrinks what exists; guardrails stop it growing back. We set three: a service control policy denying the creation of IAM users, a CI check that fails a plan introducing Action: "*" or Resource: "*" outside an allowlist, and a quarterly re-run of the last-accessed report as a scheduled job that opens tickets. Our security work bundles all three, and open-source scanners will flag most of it too — we wrote about the tools worth running.

The goal is not a perfect policy. It is a policy where every line has a reason someone can say out loud.

What to do this week

Run get-service-last-accessed-details on your five most privileged production roles. Remove every service untouched for 180 days. That is an afternoon, it is reversible, and it is the largest single reduction in blast radius available to you right now.

ConsultorIA

Want this done on your cloud?

A ten-day read-only assessment is free, and Skyline lets you see your estate on a map before you write to us.

Related articles