IAM Access Analyzer for Unused Access
Granting permissions is easy; noticing they went unused for eight months is the hard part.
Search across all documentation pages
Granting permissions is easy; noticing they went unused for eight months is the hard part.
IAM Access Analyzer's unused-access findings close that gap by continuously comparing what a role or user is allowed to do against what it actually does, and surfacing the difference as an actionable finding.
Access Analyzer has two distinct capabilities: external-access analysis (does any policy grant access to an outside principal) and unused-access analysis (does an identity hold permissions, roles, or keys it hasn't used in a configurable window).
This page focuses on the unused-access side, since that's the one that directly shrinks a policy's blast radius over time.
An unused-access analyzer runs continuously once created - no need to re-trigger it - and reports findings for unused IAM roles, unused permissions within an otherwise-active role, and unused access keys or passwords.
GenerateFindingRecommendation goes a step further and suggests the specific policy edit to remove the unused grant, turning "we think this is unused" into a concrete diff.
Treat these findings as a starting point for review, not an auto-apply switch - a permission unused in the analysis window may still be legitimately needed for an infrequent task.
Quick-reference recipe card - copy-paste ready.
{
"analyzerName": "org-unused-access",
"type": "ACCOUNT_UNUSED_ACCESS",
"configuration": {
"unusedAccess": { "unusedAccessAge": 90 }
}
}When to reach for this:
Create an unused-access analyzer, list its findings, and generate a fix recommendation.
# --- Python (boto3) ---
import boto3
aa = boto3.client("accessanalyzer")
# Create an analyzer scoped to the account, flagging access unused after 90 days.
analyzer = aa.create_analyzer(
analyzerName="org-unused-access",
type="ACCOUNT_UNUSED_ACCESS",
configuration={"unusedAccess": {"unusedAccessAge": 90}},
)
analyzer_arn = analyzer["arn"]
# List findings once the analyzer has had time to scan.
findings = aa.list_findings_v2(
analyzerArn=analyzer_arn,
filter={"findingType": {"eq": ["UnusedIAMRole", "UnusedPermission"]}},
)
for f in findings["findings"]:
print(f["id"], f["resource"], f["findingType"])
# Ask for a concrete fix on one finding.
rec = aa.generate_finding_recommendation(
analyzerArn=analyzer_arn, id=findings["findings"][0]["id"]
)
print(rec["recommendedSteps"])// --- TypeScript (AWS SDK v3) ---
import {
AccessAnalyzerClient, CreateAnalyzerCommand, ListFindingsV2Command,
GenerateFindingRecommendationCommand,
} from "@aws-sdk/client-accessanalyzer";
const aa = new AccessAnalyzerClient({});
// Create an analyzer scoped to the account, flagging access unused after 90 days.
const analyzer = await aa.send(new CreateAnalyzerCommand({
analyzerName: "org-unused-access",
type: "ACCOUNT_UNUSED_ACCESS",
configuration: { unusedAccess: { unusedAccessAge: 90 } },
}));
const analyzerArn = analyzer.arn!;
// List findings once the analyzer has had time to scan.
const findings = await aa.send(new ListFindingsV2Command({
analyzerArn,
filter: { findingType: { eq: ["UnusedIAMRole", "UnusedPermission"] } },
}));
findings.findings?.forEach((f) => console.log(f.id, f.resource, f.findingType));
// Ask for a concrete fix on one finding.
const rec = await aa.send(new GenerateFindingRecommendationCommand({
analyzerArn, id: findings.findings?.[0]?.id,
}));
console.log(rec.recommendedSteps);What this demonstrates:
type: "ACCOUNT_UNUSED_ACCESS" creates an unused-access analyzer, distinct from the external-access analyzer type (ACCOUNT/ORGANIZATION).unusedAccessAge sets the lookback window in days - permissions or roles untouched for longer than this show up as findings.ListFindingsV2 supports filtering by findingType, letting you triage unused roles separately from unused permissions.GenerateFindingRecommendation returns concrete steps, so you're not left inferring the policy edit yourself.ACTIVE to RESOLVED automatically once the underlying access is used or removed, or you can archive it manually if it's a known, accepted exception.| Finding Type | What It Flags |
|---|---|
UnusedIAMRole | A role that has never been assumed, or not assumed within the configured window |
UnusedPermission | Specific actions granted to an active role/user but never called |
UnusedIAMUserAccessKey | An access key that exists but hasn't been used to sign a request |
UnusedIAMUserPassword | A console password that hasn't been used to sign in |
Access Analyzer's two analyzer families solve different problems and are created with different type values:
type: "ACCOUNT" or "ORGANIZATION") - flags resource-based policies that grant access to a principal outside your account or organization.type: "ACCOUNT_UNUSED_ACCESS" or "ORGANIZATION_UNUSED_ACCESS") - flags permissions, roles, and keys that exist but go unexercised.They're complementary: external-access analysis answers "who else can reach this," unused-access analysis answers "what am I granting that nobody uses."
# boto3's list_findings_v2 paginates - use a paginator for accounts with
# many findings instead of assuming one call returns everything.
paginator = aa.get_paginator("list_findings_v2")
for page in paginator.paginate(analyzerArn=analyzer_arn):
for f in page["findings"]:
print(f["id"])// SDK v3 exposes the same pagination via NextToken on ListFindingsV2Command -
// loop until nextToken is undefined for large finding sets.
let nextToken: string | undefined;
do {
const page = await aa.send(new ListFindingsV2Command({ analyzerArn, nextToken }));
page.findings?.forEach((f) => console.log(f.id));
nextToken = page.nextToken;
} while (nextToken);unusedAccessAge window (or longer) before expecting complete results.ACCOUNT analyzer when you wanted ACCOUNT_UNUSED_ACCESS gets you a completely different set of findings. Fix: double-check the type parameter matches the finding category you actually want.UnusedIAMUserAccessKey findings because the user "still uses the account" - the user being active doesn't mean every one of their keys is. Fix: treat each key independently; rotate or deactivate flagged keys regardless of user activity.ListFindingsV2 - accounts with many roles can have far more findings than fit in one response page. Fix: use the boto3 paginator or loop on nextToken in SDK v3.unusedAccessAge too short - a very short window flags normal infrequent-but-legitimate access as unused, creating noise. Fix: start around 90 days and adjust based on your actual review cadence.| Alternative | Use When | Don't Use When |
|---|---|---|
| Access Analyzer unused-access findings | Ongoing, automated detection of stale grants | You need a one-time snapshot review only |
| Manual CloudTrail log review | A small, well-understood account with few roles | The account has enough activity that manual review misses things |
| IAM Access Advisor (per-service last-accessed) | Checking one role's per-service usage interactively in the console/SDK | You want continuous, account-wide automated findings |
Access Analyzer policy generation (StartPolicyGeneration) | Building a new least-privilege policy from scratch | You're pruning an existing policy rather than drafting a new one |
External-access analyzers flag resource policies that grant access to outside principals. Unused-access analyzers flag permissions, roles, and keys that exist but haven't actually been exercised. They're created with different type values and solve different problems.
It needs to observe activity across your configured unusedAccessAge window before findings are complete - don't expect a full picture immediately after creation.
It means specific actions granted to that role haven't been called, even though the role overall is active. The role itself isn't unused - just some of the permissions attached to it.
Yes, via GenerateFindingRecommendation, which returns concrete recommended steps for resolving a specific finding rather than just flagging the problem.
No. Review with the owning team first - some permissions back infrequent tasks (quarterly reports, incident response) that fall outside a typical lookback window.
The configurable lookback window, in days, that determines how long access can go unexercised before it's flagged as a finding. A common starting point is 90 days.
Both. Finding types include UnusedIAMRole, UnusedPermission, UnusedIAMUserAccessKey, and UnusedIAMUserPassword.
Yes, using ORGANIZATION_UNUSED_ACCESS as the analyzer type instead of ACCOUNT_UNUSED_ACCESS, which requires the analyzer to be created from the Organizations management or delegated admin account.
If the previously-unused access is subsequently used, or the permission/role/key is removed, the finding's status can transition from ACTIVE to RESOLVED without manual intervention.
Archive the finding rather than deleting the analyzer or ignoring it - archiving records the decision explicitly so it doesn't keep resurfacing as noise.
Access Advisor shows per-service last-accessed data for one principal, checked interactively. Access Analyzer's unused-access findings are continuous, account- or org-wide, and paired with concrete recommendations via GenerateFindingRecommendation.
Stack versions: This page was written for boto3 1.43.x (Python 3.10+) and the AWS SDK for JavaScript v3 (Node.js 18+).
Reviewed by Chris St. John·Last updated Jul 25, 2026