AWS IAM Least Privilege: Policy Design Patterns
Over-permissioned IAM roles are one of the most common findings in AWS security assessments, and they are usually the result of convenience rather than malice — a wildcard Action or Resource added during development that never gets tightened before production. This article walks through concrete patterns for designing least-privilege IAM policies: how to scope actions and resources tightly, how to use condition keys to add context-aware restrictions, and how permission boundaries and service control policies (SCPs) provide a second layer of defence when an individual policy is misconfigured.
Why wildcard policies are a persistent problem
IAM policies are additive and evaluated across every attached policy, group, role, and SCP that applies to a principal. When teams are moving fast, it's tempting to grant `"Action": "s3:*"` or `"Resource": "*"` to unblock a deployment, with a mental note to restrict it later. That note rarely gets actioned, and the wildcard becomes the permanent shape of the role.
The practical risk isn't theoretical. A compromised CI/CD credential, a leaked access key in a public repository, or a server-side request forgery (SSRF) against an EC2 instance metadata endpoint all turn an over-permissioned IAM identity into a blast radius multiplier. Least privilege doesn't prevent the initial compromise, but it bounds what an attacker can do with it.
Start from resource-level restrictions, not action lists
Most engineers start tightening a policy by trimming the action list — replacing `s3:*` with `s3:GetObject`, `s3:PutObject`. That's necessary but not sufficient. The `Resource` element deserves equal attention: an action scoped to a single object action but applied against `"Resource": "*"` still allows access to every bucket in the account.
A well-scoped policy names the specific bucket, prefix, table, or queue the role actually needs. For services that support resource-level permissions (S3, DynamoDB, SQS, SNS, Lambda, KMS, and most newer services), always supply an ARN rather than a wildcard.
Least-privilege S3 access scoped to a single bucket and prefix
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadWriteAppDataPrefix",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::app-data-prod/uploads/*"
},
{
"Sid": "ListBucketScopedToPrefix",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::app-data-prod",
"Condition": {
"StringLike": {
"s3:prefix": "uploads/*"
}
}
}
]
}Use condition keys to add context, not just resource scope
Condition keys let a policy react to the context of a request — source IP, MFA presence, tag values, encryption settings, and more — rather than only the action and resource. This is where IAM policies move from "which resource" to "under what circumstances."
Common condition keys worth knowing:
- •aws:MultiFactorAuthPresent — require MFA for sensitive or destructive actions
- •aws:SourceIp / aws:SourceVpce — restrict API calls to a corporate CIDR range or a specific VPC endpoint
- •aws:PrincipalTag / aws:ResourceTag — implement attribute-based access control (ABAC) so a single policy scales across many resources sharing a tag
- •s3:x-amz-server-side-encryption — deny unencrypted PutObject requests
- •aws:RequestedRegion — restrict actions to approved regions for data residency
- •aws:PrincipalOrgID — restrict cross-account access to principals within your AWS Organization
Deny destructive EC2 actions without MFA
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyTerminateWithoutMFA",
"Effect": "Deny",
"Action": [
"ec2:TerminateInstances",
"ec2:StopInstances"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}ABAC with tag-based conditions for scalable least privilege
Resource-by-resource policies become unmanageable once an account has hundreds of roles and thousands of resources. Attribute-based access control addresses this by granting access based on matching tags between the principal and the resource, rather than hardcoding ARNs. A single policy attached to every engineering role can then grant access only to resources tagged with that engineer's team, without a policy update every time a new resource is created.
ABAC policy: access limited to resources tagged with the caller's team
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ABACAccessByTeamTag",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:*:*:table/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/team": "${aws:PrincipalTag/team}"
}
}
}
]
}Permission boundaries and SCPs as a second layer
Identity-based policies define what a role can do; a permission boundary defines the maximum it is ever allowed to do, regardless of what identity-based policies are later attached. This matters most for roles that are allowed to create other IAM roles (a common CI/CD or platform-team pattern) — without a boundary, a role with `iam:CreateRole` and `iam:AttachRolePolicy` can create a new role with administrator access, silently escalating privilege.
Service control policies (SCPs) operate at the AWS Organizations level and apply account- or OU-wide, acting as a guardrail that no identity-based policy in that account can exceed — not even for the account root user. SCPs are commonly used to deny actions like disabling CloudTrail, leaving approved regions, or deleting the security/logging account's resources.
Permission boundary limiting a CI role to a scoped set of services
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BoundaryAllowedServices",
"Effect": "Allow",
"Action": [
"s3:*",
"lambda:*",
"logs:*"
],
"Resource": "*"
},
{
"Sid": "BoundaryDenyIAMEscalation",
"Effect": "Deny",
"Action": [
"iam:CreateUser",
"iam:CreateAccessKey",
"iam:AttachUserPolicy",
"iam:PutUserPolicy",
"iam:CreatePolicyVersion"
],
"Resource": "*"
}
]
}Building policies from observed usage, not guesswork
Hand-writing minimal policies from documentation is slow and error-prone. AWS IAM Access Analyzer can generate a policy based on the actual CloudTrail activity of a role over a chosen time window, which is a much faster starting point than guessing which actions an application needs. The generated policy still needs review — CloudTrail only captures what was exercised, so a rarely-used code path (an error handler that calls a different API, for example) may be missed.
A practical workflow: deploy the role with a broader policy in a non-production environment, exercise all code paths (including error and retry paths), run Access Analyzer's policy generation against the resulting CloudTrail events, then tighten the resource ARNs by hand before promoting to production.
- •IAM Access Analyzer — generates least-privilege policies from CloudTrail activity and flags resources shared outside the account/org
- •Access Analyzer's "unused access" findings — surfaces permissions granted but never exercised over a lookback period
- •aws iam simulate-principal-policy — tests whether a specific action/resource combination would be allowed before deploying a policy change
Common anti-patterns to flag in review
A short checklist for reviewing IAM policies, whether by hand or as part of a CI policy-linting step:
- •"Resource": "*" paired with a mutating action (Put/Delete/Create) rather than a read-only one
- •iam:PassRole granted without a resource restriction — allows a principal to hand off any role to a service, a common privilege-escalation path
- •Trust policies with a wildcard Principal or an overly broad AWS account condition
- •Long-lived IAM user access keys instead of role assumption via STS for workloads that support it
- •Policies that grant *:* or administrator-equivalent access as a shortcut during incident response and are never revoked afterward
References
Primary sources for the material above. Standards are cited by identifier so they stay findable as publishers reorganise their sites.
- AWS IAM — Security Best Practices in IAM
- AWS IAM Access Analyzer — Generate Policies Based on Access Activity
- AWS Organizations — Service Control Policies (SCPs)