Least privilege is not achieved by writing perfect policies on day one — it is achieved by granting roughly right, then pruning what usage data proves unnecessary. AWS gives you three instruments for the loop: IAM Access Analyzer external-access findings (who outside the account can reach your resources), unused-access findings (which roles, keys, and permissions sat idle), and last-accessed data plus CloudTrail-based policy generation to rewrite grants down to observed reality. Run the loop quarterly and wildcards stop accumulating.
What’s happening
Permissions only grow by default. Every incident fix, migration, and “just unblock me” adds grants, and nothing removes them — the removal side has no natural driver, so an account’s permission surface expands until an audit or a breach forces the question. The pruning loop replaces fear with data: instead of asking “might anything use this?”, you ask “did anything use this in 90 days?”, which is answerable.
The tooling splits by direction. External access analyzers examine resource policies — S3 buckets, KMS keys, IAM role trust policies, SQS queues, Lambda permissions, and more — and use automated reasoning to flag anything reachable from outside your zone of trust (the account or the organization). These are free and should exist in every account, since a public bucket or an over-trusting role is exactly the finding you want the same hour it appears. Unused access analyzers watch principals instead: roles and users that have not authenticated, access keys and passwords idle past your threshold, and granted-but-unused services and actions. Fair warning on cost mechanics: unused-access analysis is a paid feature, charged per IAM role or user analyzed per month, so the account’s principal count drives the bill — reasonable for the accounts that matter, worth scoping deliberately. Alongside the analyzers, last-accessed data on any role shows when each service was last used (with action-level detail for some services including S3, EC2, IAM, and Lambda), and Access Analyzer can generate a policy from a role’s actual CloudTrail history — the single fastest way to shrink a PowerUserAccess role to what it demonstrably does. Full capability list lives in the IAM documentation.
What to check first
- Does an external-access analyzer exist in each region you use? No analyzer means public-exposure findings are nobody’s job. Zero findings on a fresh analyzer is information; zero analyzers is negligence.
- The obvious over-grants. Roles wearing
AdministratorAccessorPowerUserAccessthat belong to applications, not humans. - Idle principals. Roles and users with no authentication in 90+ days are the cheapest wins — whole principals, not individual actions.
- Whether CloudTrail history is intact. Policy generation and honest “unused” verdicts both depend on the trail actually covering the window — see our CloudTrail and GuardDuty baseline.
How to verify
# Analyzers in this region, and their type
aws accessanalyzer list-analyzers \
--query 'analyzers[].[name,type,status]' --output table
# Active findings (external access)
aws accessanalyzer list-findings \
--analyzer-arn arn:aws:accessanalyzer:ca-central-1:111122223333:analyzer/org-analyzer \
--filter '{"status": {"eq": ["ACTIVE"]}}'
# Service last-accessed for one role (two-step, asynchronous)
aws iam generate-service-last-accessed-details \
--arn arn:aws:iam::111122223333:role/app-role
aws iam get-service-last-accessed-details --job-id <job-id-from-previous>
Console paths: IAM > Access Analyzer for findings, and on any role, the Last Accessed tab — the sorted “not accessed in tracking period” list at the bottom is the pruning shortlist. Note the tracking window for last-accessed data is up to 400 days.
The runbook
- Create the external-access analyzer first, org-wide if you have Organizations. Triage its findings before any pruning: cross-account trust nobody recognizes and public resources outrank every unused permission.
- Delete or quarantine idle principals. For roles unused 90+ days: strip policies or attach an explicit deny-all, wait a cycle, then delete. Deactivate idle keys the same way — reversible first, permanent later.
- Turn on unused-access analysis where it pays. Production and shared-services accounts first; the per-principal pricing makes org-wide-everything a deliberate choice, not a default.
- Shrink the worst roles with generated policies. IAM > Roles > (role) > Generate policy builds a policy from up to 90 days of CloudTrail activity. Review it — generation covers the services CloudTrail saw, and infrequent-but-legitimate actions (yearly DR runbooks) need manual re-adding before you swap the policy.
- Institutionalize the loop. A quarterly review of new findings, a standing rule that expansion PRs cite a denied action, and archive-with-reason for accepted findings so the dashboard stays meaningful.
When this is the wrong move
Do not prune during an incident or a migration window — “unused for 90 days” is exactly what disaster-recovery permissions look like the week before you need them; tag DR and break-glass roles as exempt and test them on their own schedule instead. And if the account has never had basics — no MFA story, root keys still alive — fix those before financing an unused-access subscription; sequencing matters.
Common mistakes
- Trusting a 30-day window: quarterly billing jobs, yearly audits, and DR paths all look unused right up until they are not.
- Shipping a generated policy unreviewed and breaking the rare-but-critical code path CloudTrail never witnessed in the window.
- Archiving external-access findings without recording why, so the next reviewer re-litigates every bucket.
- Pruning permissions while leaving the idle principals themselves alive — an unused role with a live trust policy is still an attack surface.
Keeping it under control
- Alarm on new external-access findings; they are security events, not backlog.
- Put last-accessed review into the quarterly access review that auditors already require — same meeting, better data.
- Require new roles to start from a scoped template, not
PowerUserAccess, so the pruning debt stops compounding. - Track a simple metric per account — active findings plus admin-attached roles — and make someone own its direction.
Least privilege is a verb: accounts that run the loop get tighter every quarter, and accounts that rely on good intentions get wider. 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.