IAnswerable

Write IAM Policies That Aren't Just Star Star

01 Jan 2026

"Action": "*" on a policy is not a starting point you’ll fix later. It’s a starting point you’ll forget about until a security review or an incident finds it. Here’s how to narrow one down without breaking the app it protects.

Don’t write the policy from a blank page

Guessing which permissions an app actually needs and writing a policy by hand is how you end up either too broad (defeats the purpose) or too narrow (breaks production at 2am). Instead, let AWS tell you what’s actually being used.

1. Turn on CloudTrail if it isn’t already

You need a record of every API call the role or user actually makes. If CloudTrail’s been running for a while, you already have this data.

2. Generate a policy from actual usage

IAM Access Analyzer’s policy generation feature reads CloudTrail logs for a role over a time window and produces a policy scoped to exactly the actions it actually called:

aws accessanalyzer start-policy-generation \
  --policy-generation-details principalArn=arn:aws:iam::123456789012:role/my-app-role \
  --cloud-trail-details '{"trails":[{"cloudTrailArn":"arn:aws:cloudtrail:us-east-1:123456789012:trail/my-trail"}],"startTime":"2026-08-01T00:00:00Z","endTime":"2026-09-01T00:00:00Z"}'

This gives you a real policy based on real behavior, not a guess. Run it over a window long enough to cover your app’s actual usage patterns — a month is reasonable for most apps, longer if you have things that only run monthly (backups, batch jobs) that you don’t want to miss.

3. Don’t just paste the generated policy in and walk away

The generated policy reflects what happened during the window, not what should be allowed. Review it for two things:

{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::my-app-bucket/uploads/*"
}

Not arn:aws:s3:::*. Scope to the specific bucket and prefix the app actually touches.

4. Roll it out safely, not in one cutover

Don’t replace the production policy in place. Attach the new, narrower policy to a test role or a canary instance first, and run your actual test suite and a normal traffic sample against it. IAM’s policy simulator (aws iam simulate-principal-policy) lets you check specific actions against a candidate policy without deploying anything:

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/my-app-role \
  --action-names s3:DeleteObject \
  --resource-arns arn:aws:s3:::my-app-bucket/uploads/file.png

Only cut production over once you’ve confirmed nothing that should work has been denied.

5. Use Deny for the things that should never happen, regardless of Allow

Least privilege isn’t just narrowing Allow — an explicit Deny on a genuinely dangerous action (deleting the production database, modifying IAM itself) is a backstop that holds even if a future Allow statement gets added carelessly. Deny always wins over Allow in IAM evaluation, which makes it the right place to put your hard lines.

Keep it from drifting back

Access Analyzer also runs continuously and flags unused permissions in existing policies going forward, not just at generation time — turn that on and revisit flagged findings on a schedule (quarterly is reasonable), because the whole point of this exercise is undone if nobody looks again after this week.