A role with AdministratorAccess attached "works" the same way a master key works on every door in a building — convenient, right up until that one credential leaks and every door is open at once. Least privilege is the practice of never handing out more than a given task actually needs, so a single compromised role, key, or function is a contained problem instead of an account-wide one.
The definition, in AWS's own words
Per AWS's IAM security best practices: "When you set permissions with IAM policies, grant only the permissions required to perform a task." That's the whole idea — every Action and every Resource in a policy should trace back to something the identity actually needs to do.
It's fine to start broad
AWS's own guidance explicitly doesn't expect a perfectly scoped policy on day one: "You might start with broad permissions while you explore the permissions that are required for your workload or use case. As your use case matures, you can work to reduce the permissions that you grant." Least privilege is a direction you work toward, not a one-shot requirement — the mistake isn't starting broad, it's never coming back to narrow it.
AWS managed policies are a reasonable starting point for exactly this reason, but they're written to cover common cases across every AWS customer, not your specific workload — AWS's own docs note they "might not grant least-privilege permissions for your specific use cases," and recommend narrowing with a customer managed policy once you know what you actually use.
What narrowing actually looks like
A policy written for "works, deal with it later" and one written for least privilege often differ in three specific places:
// Broad — works, but grants far more than needed
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
// Narrow — the same feature, scoped to what it actually does
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-app-uploads/*",
"Condition": {
"Bool": { "aws:SecureTransport": "true" }
}
}- Action: a wildcarded service (
s3:*) becomes the specific actions the code actually calls — the difference between a bug that reads an object it shouldn't and one that can delete the entire bucket. - Resource:
"*"becomes the exact ARN (or ARN pattern) involved, so the permission physically can't reach any other bucket in the account. See What Is an ARN? if that syntax is new. - Condition: added on top, restricting when the permission applies — here, only over TLS. Condition doesn't replace narrow Action/Resource scoping, it layers on top of it.
The tools AWS built for this
Doing this narrowing entirely by hand doesn't scale, so AWS ships two things that automate most of it:
- IAM Access Analyzer policy generation — points at a role's actual CloudTrail activity and generates a fine-grained policy from what it genuinely called, rather than what might theoretically be needed. Review and test the generated policy before it replaces anything broader.
- Last accessed information (Access Advisor) — shows which services and actions a role or user has actually used recently, so permissions nobody has touched in months become visible and removable instead of quietly accumulating forever.
Try it yourself
Build a scoped-down statement with IAM Policy Generator, or paste an existing policy into IAM Policy Viewer to see exactly which actions and resources it grants before deciding what to trim. For the fundamentals of a policy document's structure, see IAM Policy Basics.