Foundations
Designing least-privilege IAM policies
Start from the task, not the service: inventory the API calls, scope every action to concrete ARNs, add conditions where the request context allows, and use access data to remove what is never used.
Published · Updated .
“Least privilege” is easy to agree with and hard to practise. The difficulty is rarely the policy syntax; it is knowing exactly which API calls a workload makes, on which resources, under which circumstances. This guide is a repeatable method for getting from a vague requirement (“the reporting job needs S3”) to a policy you can defend in a review.
1. Start from the task, not the service
Write the requirement as a list of operations on named resources before opening any editor. For a nightly reporting job that reads raw data and writes a summary, that might be:
- List and read objects under
raw/inamzn-s3-demo-bucket. - Write one object per day under
reports/in the same bucket. - Write its own logs to CloudWatch Logs.
Each line maps to a few IAM actions. The Service Authorization Reference lists, for every service, the actions, the resource types each action supports, and the condition keys it understands. That last detail matters: if an action does not support resource-level permissions, its only valid Resource is "*", and you need another way to narrow it.
2. Scope actions explicitly
Prefer an explicit list of actions over wildcards. s3:Get* looks harmless, but it includes actions such as reading bucket policies and ACLs, and future Get actions AWS adds later are granted automatically. Explicit lists also make reviews meaningful: a reviewer can see that s3:DeleteObject is absent.
Wildcards are reasonable when the set is genuinely what you want (ec2:Describe* for a broad read-only auditor, for example), but even there check what the wildcard expands to. ec2:Describe* includes ec2:DescribeInstanceAttribute, which can return instance user data, a place where bootstrap scripts sometimes leave credentials. The EC2 inventory example lists actions individually for that reason.
3. Scope every resource to an ARN
Most data-plane actions accept specific ARNs. The two most common scoping mistakes in S3 are confusing bucket-level and object-level actions: s3:ListBucket takes the bucket ARN, while s3:GetObject takes object ARNs (bucket/* or a prefix). A policy that puts s3:GetObject on the bucket ARN grants nothing, and the usual “fix” is to widen it to "*".
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListRawAndReports",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket",
"Condition": {
"StringLike": {
"s3:prefix": [
"raw/*",
"reports/*"
]
}
}
},
{
"Sid": "ReadRawData",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/raw/*"
},
{
"Sid": "WriteReports",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/reports/*"
}
]
}Three statements, each named after its purpose. Note that reading and writing are separated: the job cannot overwrite its own input, and cannot read anything it writes elsewhere. Statement IDs (Sid) are optional, but they make CloudTrail investigations and reviews much easier.
4. Add conditions where the request context allows
When an action can only take "*", or when the resource alone is not specific enough, conditions narrow the grant. Useful defaults:
aws:RequestedRegionto keep a workload in the Regions it runs in.aws:ResourceTag/<key>andaws:PrincipalTag/<key>for tag-based (ABAC) policies; see the ABAC example.iam:PassedToServicewhenever you grantiam:PassRole.
The condition keys guide covers operators and pitfalls in detail.
5. Separate roles by lifecycle
A frequent source of over-permission is one role doing two jobs. The role that deploys a Lambda function needs lambda:UpdateFunctionCode and iam:PassRole; the function's execution role needs neither. Keep deployment roles, runtime roles and human access roles separate, and give each the narrowest policy for its job.
6. Refine from evidence
Policies drift towards over-permission as requirements change and nobody removes old grants. AWS provides evidence to refine them:
- Last accessed information in the IAM console (and the
generate-service-last-accessed-detailsAPI) shows which services, and for some services which actions, a role has used, and when. - IAM Access Analyzer policy generation can draft a policy from a role's CloudTrail activity. Treat the draft as a starting point: it only knows what happened during the analysed period.
- IAM Access Analyzer unused access findings (a paid analyzer) report unused roles, keys and permissions continuously.
JOB=$(aws iam generate-service-last-accessed-details --arn arn:aws:iam::111122223333:role/<role-name> --query JobId --output text)
# The report is generated asynchronously: wait until JobStatus is COMPLETED
until [ "$(aws iam get-service-last-accessed-details --job-id $JOB --query JobStatus --output text)" = COMPLETED ]; do sleep 2; done
aws iam get-service-last-accessed-details --job-id $JOB --query 'ServicesLastAccessed[?TotalAuthenticatedEntities>`0`].[ServiceNamespace,LastAuthenticated]' --output tableCommon mistakes
- Granting
service:*“temporarily”. Temporary grants rarely get removed. If you must, make it expire with aDateLessThancondition onaws:CurrentTime, and replace it with an explicit list based on last-accessed data. - Ignoring encryption keys. Reading an SSE-KMS object also requires
kms:Decrypton the key. Add it on the specific key ARN rather than granting KMS broadly. - Testing only the happy path. A least-privilege test includes calls that must fail.
Verification checklist
- Validate the JSON with IAM Access Analyzer and resolve ERROR and SECURITY_WARNING findings.
- Exercise every intended operation as the role (a test environment is fine).
- Exercise at least one operation that must be denied, and confirm the error mentions the right policy type.
- Re-check last-accessed data after a representative period and remove anything unused.
Next
Validating policies with IAM Access Analyzer shows how to automate the first verification step in CI.