I spend a lot of time looking at IAM policies. Real ones — from production accounts, staging environments, CI/CD pipelines. After a while, the same mistakes keep appearing. Not because teams are careless. Because IAM's policy language is expressive enough that you can write something that looks reasonable and still be wrong.
Here are four patterns I keep finding, why they're dangerous, and how to catch them.
1. Action: "*" with Resource: "*"
This is the obvious one, but it hides in less obvious places. It's not always AdministratorAccess. Sometimes it's a Lambda execution role that started as a placeholder during development. Six months later it's still there, running in production, with full access to every service.
What to check: Does this role actually need to touch DynamoDB? SQS? KMS? If the Lambda only writes to CloudWatch Logs and reads from one S3 bucket, scope it to those two services.
The fix isn't Action: "s3:*" — that still gives the role permission to delete every bucket in the account. Narrow it to the specific actions the function calls: s3:GetObject, s3:PutObject.
2. iam:PassRole without conditions
PassRole is the permission that says "this principal can pass a role to an AWS service." It's what lets a Lambda function assume its execution role, or an EC2 instance launch with an instance profile.
Without conditions, PassRole with a wildcard resource means the principal can pass any role in the account. A developer with PassRole on * can launch an EC2 instance with the account's admin role attached.
What to check: Every PassRole statement should have a Resource field that lists specific role ARNs, and ideally a Condition with iam:PassedToService scoping which services can receive the role.
3. s3:GetObject on * in a role's policy
A role that can read any S3 object in any bucket. Including the bucket where your CloudTrail logs land. Including buckets owned by other teams that might contain customer data.
This often happens because someone created a role to read from one specific bucket, typed "Resource": "*" to get it working, and never came back to tighten it.
What to check: Every s3:GetObject statement should list specific bucket ARNs. If the role needs access to multiple buckets, list them explicitly. The extra thirty seconds of typing saves you from a blast radius problem later.
4. Missing condition keys on broad permissions
A role with ec2:TerminateInstances on * is a role that can terminate every EC2 instance in the account. Add "Condition": {"StringEquals": {"aws:ResourceTag/Environment": "staging"}} and suddenly that same permission only applies to instances tagged Environment: staging.
Condition keys like aws:ResourceTag, aws:PrincipalTag, and aws:SourceIp are how you write policies that are broad where they need to be and tight everywhere else. But most policies I see don't use them at all.
What to check: For any Action that includes Delete* or Terminate*, look at whether a resource tag condition would reduce the blast radius. ABAC (attribute-based access control) isn't just a buzzword — it's how teams with 200+ roles keep permissions manageable.
How to catch these systematically
Manual review works for one role. It doesn't scale to fifty. And it definitely doesn't work when policies change across deploys and nobody reviews the diff.
The Shieldly free demo at shieldly.io/app/iam runs AI-Powered analysis on any IAM policy and flags wildcard actions, missing conditions, overly broad resources, and PassRole risks — no signup needed. Paste a policy and get findings with remediation steps.
For teams: the CLI (@shieldly/cli) and GitHub Action slot into CI/CD. The CDK Construct (@shieldly/cdk-guard) catches issues before deployment. And the VS Code extension flags them while you're writing the policy.
I built Shieldly because I spent too many Friday afternoons staring at JSON policy documents wondering which wildcard was the problem. If these patterns look familiar, give the demo a try.
Top comments (0)