Least privilege has a reputation for costing a week of tickets, and it earns that reputation whenever someone writes the minimal policy first and finds out what was missing by breaking people's work. The order that avoids it is the other way round. Cap the damage first, let the team work, collect evidence about what they actually used, and tighten against the evidence.
Start with a ceiling, not a floor
Before narrowing a single policy, put a guardrail above everything. A service control policy offers central control over the maximum available permissions for the IAM users and roles in your organization, and it grants nothing. AWS is unambiguous on that point, since no permissions are granted by an SCP. A user with no identity-based policy still has no access however permissive the SCP is.
That property is exactly what makes it safe to be generous underneath. A developer role with broad permissions, inside an account whose SCP denies operating outside your Regions, denies deleting CloudTrail trails and denies detaching the guardrails themselves, has a blast radius you can describe in a sentence. Two limits are worth memorising. SCPs do not affect users or roles in the management account, and they do not affect any service-linked role.
Roll them out the way AWS recommends, by creating an OU and moving accounts into it one at a time rather than attaching to the organization root and finding out the hard way.
Where a team creates its own roles, a permissions boundary is the same idea one level down. If both a permissions boundary and an SCP are present, then the boundary, the SCP and the identity-based policy must all allow the action.
Read what was actually used
IAM records last accessed information, and it is the quickest way to see how far a policy sits from what the work needs.
JOB_ID=$(aws iam generate-service-last-accessed-details \
--arn arn:aws:iam::123456789012:role/deploy-role \
--granularity ACTION_LEVEL \
--query JobId --output text)
aws iam get-service-last-accessed-details --job-id "$JOB_ID"
Know what the data is before you cut anything based on it. Recent activity appears within four hours. The tracking period for service-level information is at least 400 days. Action-level information is narrower, since it reports service management actions and it started at different dates per service, April 2020 for Amazon S3 actions, April 2021 for Amazon EC2, IAM and Lambda, and May 2023 for everything else. Action last accessed information is not available for any data plane event, so S3 object reads never appear. The iam:PassRole action is not tracked at all.
Two further limits change how you read a report. It includes all attempts to access an API, not only the successful ones, so an entry is not proof that the call worked. And it only covers services allowed by the identity's own policies, since access allowed by resource-based policies, ACLs, SCPs, permissions boundaries and session policies is excluded.
Generate the first draft from the log
IAM Access Analyzer can write the draft for you. It reviews your CloudTrail logs and generates a policy template containing the permissions an entity used within a date range you specify, up to 90 days.
aws accessanalyzer start-policy-generation \
--policy-generation-details '{"principalArn":"arn:aws:iam::123456789012:role/deploy-role"}' \
--cloud-trail-details '{
"trails":[{"cloudTrailArn":"arn:aws:cloudtrail:eu-west-1:123456789012:trail/org-trail","allRegions":true}],
"accessRole":"arn:aws:iam::123456789012:role/service-role/AccessAnalyzerMonitorServiceRole",
"startTime":"2026-05-25T00:00:00Z"
}'
Treat the output as a draft and nothing more. For some services Access Analyzer identifies the individual actions in your CloudTrail events and lists them, and for the rest it falls back to last accessed information at service level and expects you to choose the actions yourself. Data events are not identified, iam:PassRole is not included, and AWS says directly that you should not use policy generation for auditing and should use CloudTrail for that instead. A trail has to be enabled for the account before any of this works, and a shorter date range generates faster.
Find what nobody uses any more
An unused access analyzer is the continuous version of the same idea. It monitors all IAM roles and users in your organization and accounts and generates findings for unused roles, unused access keys for IAM users and unused passwords for IAM users, and for active identities it reports the services and actions they are not using. You set the age in days that counts as unused, anywhere from 1 to 365.
aws accessanalyzer create-analyzer \
--analyzer-name unused-access-90d \
--type ORGANIZATION_UNUSED_ACCESS \
--configuration '{"unusedAccess":{"unusedAccessAge":90}}'
Unlike external access findings, unused access findings do not change based on Region, so creating one in each Region is not required. It is a charged feature, billed on the number of IAM roles and users analyzed per analyzer per month, and service-linked roles are not analyzed for unused access and are not counted in that total.
Check the tightening before you ship it
The failure mode of a least privilege project is a policy change that quietly removes something in use, discovered by a developer at the worst possible moment. Access Analyzer has checks that turn that into a pipeline step rather than a discovery.
# fails if the new policy grants access the existing one did not
aws accessanalyzer check-no-new-access \
--existing-policy-document file://current-policy.json \
--new-policy-document file://new-policy.json \
--policy-type IDENTITY_POLICY
# fails if the policy allows an action you consider critical
aws accessanalyzer check-access-not-granted \
--policy-document file://new-policy.json \
--access actions="iam:DeleteRole","kms:ScheduleKeyDeletion" \
--policy-type IDENTITY_POLICY
There is a third, check-no-public-access, which tests whether a resource policy can grant public access to a specified resource type. Alongside them, policy validation checks a policy against IAM policy grammar and AWS best practices and returns errors, security warnings, general warnings and suggestions. Each check for new access is charged per request, so run them on the diff rather than on every policy in the account.
The part that keeps the team working
Tighten one role at a time, and leave the previous policy in place until the new one has run beside it through a full cycle of the work that role does. Give people a documented break-glass path with a short session and an alarm on its use, so that being blocked has a fifteen-minute answer instead of a ticket. And keep reading last accessed data after each tightening, because it records attempts rather than successes, which means a denial you caused shows up there as evidence before it shows up as a complaint.
Done in that order the project is unremarkable, which is the goal. If you want the guardrails and the evidence loop set up once and properly, that is security and compliance work, and it sits naturally alongside ongoing AWS cloud management.
Talk to the engineer who will own your stack.
No account managers, no offshore handoff. Senior DevOps, direct. Tell us what you are dealing with and you get a straight answer.
Related Articles
How to Detect and Respond to a Compromised Linux Server
A practical incident response guide for Linux servers: identifying signs of compromise, initial triage, evidence preservation, containment, rootkit detection, and writing an incident report.
SecurityEleven npm Packages Compromised in a 53 Minute Attack That Steals Every Credential Your Build Host Can Reach
On August 4, 2026 a worm pushed malicious versions of eleven npm caching packages inside a 53 minute window, harvesting npm tokens, GitHub PATs, AWS credentials, Kubernetes service account tokens and SSH keys. The headline was keyv and its 604 million monthly downloads, but keyv was the safest package on the list: its malicious release was a major version bump that no caret range accepts. The other ten were patch bumps, silently eligible for every dependency range in the ecosystem. That distinction, not the download count, decided who got hit. This is a practical guide to the defenses that actually change the outcome: what your semver range really grants, why npm install and npm ci are not interchangeable, when to disable install scripts and what breaks when you do, and how to check a tree you already have.
SecurityAWS WAF Configuration for Web Application Security
Deploy and configure AWS WAF with managed rule groups, custom rules, rate limiting, and bot control to protect web applications from common threats.