Least privilege is only as good as the policies you write, and most access bugs are policy bugs.
A policy is a JSON document with a Version (always "2012-10-17") and a list of statements. Each statement has:
Effect:AlloworDeny.Action: API actions likes3:GetObject; wildcards such ass3:Get*work.Resource: the ARNs the actions apply to.Condition(optional): extra tests on request context keys, such asaws:SourceIp,aws:RequestedRegion,aws:PrincipalTag/teamor service keys likes3:prefix.
Identity-based policies attach to users, groups and roles; they have no Principal. Resource-based policies (S3 bucket policies, SQS queue policies, KMS key policies, role trust policies) attach to the resource and must name a Principal.
Policies come as AWS managed, customer managed (reusable, versioned) or inline (embedded in one identity). Prefer customer managed for anything you write yourself.
Evaluation basics: everything starts denied; an applicable Allow permits the action; an explicit Deny anywhere overrides every allow. That makes Deny statements good guardrails.
A classic gotcha is resource granularity. In S3, s3:ListBucket applies to the bucket ARN arn:aws:s3:::bucket, while s3:GetObject applies to objects, arn:aws:s3:::bucket/*. Put the wrong ARN in and you get AccessDenied despite the right action.
Write policies narrow, then use IAM Access Analyzer to validate them and to generate a least-privilege policy from the actions a role actually used in CloudTrail.
Example
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListReportsPrefix",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::acme-data",
"Condition": {"StringLike": {"s3:prefix": ["reports/*"]}}
},
{
"Sid": "ReadWriteReports",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::acme-data/reports/*"
},
{
"Sid": "NeverDelete",
"Effect": "Deny",
"Action": "s3:DeleteObject",
"Resource": "arn:aws:s3:::acme-data/*"
}
]
}Remember
- Statement = Effect + Action + Resource (+ Condition)
- Resource policies name a Principal; identity policies don't
- Default deny, allow grants, explicit deny always wins
- Bucket-level and object-level S3 actions need different ARNs
Watch out for
- Granting s3:GetObject on arn:aws:s3:::bucket (no /*), which matches no objects.