Least privilege fails in practice for a simple reason: granting permissions is a one-time decision made under uncertainty, and revoking them requires confidence that nothing will break. Most teams grant broadly upfront because they don’t know exactly what a role will need, and then never revisit it, because finding out what’s actually being used requires visibility that doesn’t exist by default.
Why permissions accumulate
Roles and users rarely lose permissions once granted. Common patterns:
- A role gets
AdministratorAccessor a broad managed policy during initial setup “to unblock things,” with a plan to tighten it later that never happens - A permission was needed for a one-time migration or debugging session and was never removed afterward
- A service’s responsibilities changed, but its IAM policy was never updated to match — it still holds access relevant to what it used to do
- Permissions get copied from an existing role as a starting template, inheriting whatever excess that role had accumulated
None of these are unusual mistakes. They’re the predictable result of granting being cheap and auditing being expensive, unless you have tooling that makes the audit cheap too.
Finding unused access with IAM Access Analyzer
AWS IAM Access Analyzer’s unused access analysis is built specifically for this problem. Once enabled, it continuously analyzes accounts and surfaces:
- Unused IAM roles
- Unused access keys on IAM users
- Unused passwords on IAM users
- For active roles and users, specific unused services and actions within their attached policies
This last category is the one that matters most for least privilege work — it doesn’t just tell you a role exists and might be over-permissioned in theory, it tells you which specific granted permissions haven’t actually been exercised, based on real access activity. Findings consolidate into a centralized dashboard, so security teams can prioritize accounts by finding volume instead of manually reviewing every policy by hand.
Two details from AWS’s documentation that change how you plan this work. First, the unused access analyzer has a configurable tracking period of 1 to 365 days, and it only evaluates entities that have existed for the whole period (AWS docs: create an unused access analyzer). Second, unused access analysis is a paid feature billed per IAM role or user analyzed per month, unlike external access findings, which are free (AWS pricing page). Check the pricing page for the current rate in your region.
Turning findings into safe policy changes
Finding unused permissions is the easy part. Removing them without breaking something is where teams get cautious, and reasonably so — an unused-in-the-analysis-window permission isn’t necessarily an unused-forever permission if it backs an infrequent process like a quarterly batch job or an annual compliance export.
A safer sequence:
- Extend the observation window before acting on any single finding — a permission unused in a 30-day window might still be legitimate for something that runs monthly or quarterly
- Cross-reference against known infrequent processes — check with the team that owns the role about anything that runs on a schedule longer than your analysis window before removing access tied to it
- Use policy generation, not just manual editing — Access Analyzer policy generation builds a policy template from the actions an entity performed in your CloudTrail logs over a period of up to 90 days, which is a better starting point than hand-editing an existing broad policy. It has limits: it needs a CloudTrail trail, it does not identify action-level activity for data events such as S3 object reads, it does not include
iam:PassRole(CloudTrail does not track it), and it counts denied actions as activity. Review the generated template rather than attaching it as-is (AWS docs: policy generation) - Apply changes in a way that’s reversible — deploy the tightened policy, monitor for access denials over a follow-up period, and keep the prior policy version available to roll back quickly if something legitimate breaks
Where least privilege breaks down structurally
Two patterns undermine least privilege even with good tooling:
Wildcard actions and resources. A policy using "Action": "s3:*" or "Resource": "*" defeats granular analysis because it’s technically “using” a category of permission broadly enough that unused-access tooling has less to work with. Scoping policies to specific actions and specific resource ARNs, rather than wildcards, is what makes the analysis actually useful later.
Shared roles across multiple purposes. A single role used by several different services or functions accumulates the union of everything any of them needs, and removing anything requires confirming none of the other consumers depend on it. Splitting roles by function, even when it means more roles to manage, keeps each one’s actual permission needs legible.
A practical checklist
- Enable IAM Access Analyzer’s unused access analysis across your accounts, not just external access analysis
- Prioritize review by finding volume, starting with roles holding the broadest permissions
- Extend your observation window before removing access tied to infrequent processes
- Replace wildcard actions and resources with specific ARNs and actions where feasible
- Split multi-purpose shared roles so permission needs stay attributable to a single function
- Treat policy tightening as a monitored, reversible change, not a one-way cutover
Least privilege isn’t a policy you write once. It’s a permissions posture that decays by default unless something is actively pulling it back toward minimal, which is what continuous unused-access analysis is actually for.
