Every AWS account that survives past its first year converges on roughly the same dozen IAM policies: an app role scoped to one bucket, a read-only auditor, a region lockdown, a tag-based EC2 policy for developers, an MFA gate, and a handful of others. Below are the twelve we end up writing in nearly every environment, with working JSON for the ones people most often get wrong. Copy them as starting points, then tighten the resources to your ARNs.
What’s happening
IAM policies fail in two directions. Too loose, and the account accumulates Action: "*" grants that pass no audit and turn any leaked credential into a full compromise. Too tight, and every deploy generates an Access Denied ticket until someone “fixes” it with a wildcard — arriving at too loose by a longer road. The escape is a small library of proven shapes: policies where the actions, resources, and conditions are already balanced, so each new requirement is a variable substitution rather than a blank JSON editor. AWS evaluation rules reward this — explicit deny always wins, Condition blocks make grants situational, and resource ARNs do the real scoping work. The policy reference in the IAM documentation covers the grammar; the shapes below cover the practice.
What to check first
Before writing anything new, inventory what exists — most accounts already contain three half-versions of every policy below:
- Count customer-managed vs inline policies. Inline policies hide from reuse and review; anything used twice should be customer-managed.
- Find the wildcards. Any policy with
"Action": "*"on"Resource": "*"outside the admin role is the first thing to replace. - Check for AWS managed policies doing work they are too broad for.
AmazonS3FullAccesson an app role is the classic. - Look at what is actually used. Last-accessed data tells you which grants are dead weight before you redesign.
How to verify
# Inventory customer-managed policies and where they are attached
aws iam list-policies --scope Local \
--query 'Policies[].[PolicyName,AttachmentCount]' --output table
# Dump a policy's current default version
aws iam get-policy-version \
--policy-arn arn:aws:iam::111122223333:policy/app-s3-scoped \
--version-id v1
# Find inline policies on a role
aws iam list-role-policies --role-name app-role
Console path for attachment review: IAM > Policies > (policy) > Entities attached. Validate new JSON with the policy checks built into the console editor — it flags invalid actions and over-broad resources before you save.
The runbook
- App role scoped to one bucket prefix — the most-written policy in AWS. Note the split:
ListBucketon the bucket, object actions on the prefix.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::prod-assets",
"Condition": {"StringLike": {"s3:prefix": ["uploads/*"]}}
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::prod-assets/uploads/*"
}
]
}
- Region lockdown deny — stops resources appearing in regions nobody monitors. Exempt the global services that authenticate through us-east-1.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"NotAction": ["iam:*", "sts:*", "organizations:*", "route53:*",
"cloudfront:*", "support:*", "budgets:*"],
"Resource": "*",
"Condition": {"StringNotEquals": {"aws:RequestedRegion": ["ca-central-1", "us-east-1"]}}
}
]
}
- Tag-based EC2 start/stop for developers — operate only instances tagged to your team, without touching production.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ec2:StartInstances", "ec2:StopInstances", "ec2:RebootInstances"],
"Resource": "arn:aws:ec2:*:111122223333:instance/*",
"Condition": {"StringEquals": {"aws:ResourceTag/team": "platform"}}
},
{
"Effect": "Allow",
"Action": "ec2:DescribeInstances",
"Resource": "*"
}
]
}
- Read-only auditor — start from the AWS managed
ReadOnlyAccessor the tighterSecurityAudit, then add a deny for data-plane reads (s3:GetObject,dynamodb:GetItem) if the auditor should see configuration but not customer data. - Deny-unless-MFA — the
BoolIfExistsdeny that makes MFA real rather than suggested; the full shape and its pitfalls are in our MFA enforcement guide. - Self-service credentials — allow each IAM user to manage only their own password, access keys, and MFA device using
arn:aws:iam::*:user/${aws:username}as the resource. - Human assume-role-only policy — humans get
sts:AssumeRoleon named role ARNs and nothing else; all real permissions live on the roles. - Finance and billing read —
ce:Get*,ce:Describe*,budgets:View*,cur:Describe*for the people who ask about the bill, so they stop asking engineers. - CI/CD deploy role — scoped to one service: the specific ECS service or Lambda functions, the specific ECR repo,
iam:PassRolerestricted to the one task role ARN.PassRoleon*in a pipeline is privilege escalation in waiting. - TLS-only bucket policy — a resource policy with
"Effect": "Deny"when"aws:SecureTransport": "false", required by most compliance frameworks. - Single-key KMS usage —
kms:Decrypt,kms:GenerateDataKeyon one key ARN, paired with a key policy that grants the account’s IAM layer. - Break-glass admin —
AdministratorAccesson a dedicated role with a trust policy requiring MFA, every use of which pages someone via a CloudTrail-driven alarm.
When this is the wrong move
Hand-rolling all twelve is wrong if the organization is about to adopt Organizations-wide guardrails — the region lockdown and several denies belong in SCPs, once, rather than in every account. And if humans still authenticate as IAM users, policies 5-7 are patches on a model worth replacing with Identity Center instead of perfecting.
Common mistakes
- Putting
s3:ListBucketonbucket/*orGetObjecton the bare bucket ARN — each action binds to a specific ARN shape. - Forgetting the
NotActionexemptions in the region deny, which locks everyone out of IAM and billing. - Granting
iam:PassRoleon*to CI roles — the quiet privilege escalation in most pipelines. - Using inline policies for anything reused; they cannot be versioned, shared, or reviewed in one place.
Keeping it under control
- Keep the twelve as customer-managed policies in infrastructure-as-code, so drift is a diff, not archaeology.
- Name policies by scope, not team folklore:
app-prod-assets-rw, notsteve-policy-2. - Review
AttachmentCountquarterly and delete anything at zero. - Let unused-access findings drive pruning rather than intuition — grants nobody exercised in 90 days rarely deserve to stay.
Twelve shapes cover most of what an account needs; everything past that should be a deliberate exception with an owner and a reason. If nobody on your team truly owns the AWS environment end to end, that is the root of most of these problems — Stack Harbor runs AWS accounts daily under AWS Operations, and you can book an AWS review.