The 7 IAM Misconfigurations We See in Almost Every AWS Account
Implementing least privilege is one of the hardest things to get right in AWS. Not because engineers do not care — because the action graph is genuinely unknowable from memory. To deploy a single Lambda you need lambda:CreateFunction and iam:PassRole, s3:GetObject, logs:CreateLogGroup, logs:CreateLogStream, and a dozen more. So teams default to broad permissions to ship, and never come back to tighten them.
After looking at a lot of IAM, the same seven patterns show up again and again. Here is each one, why it is dangerous, and the fix.
1. Action: "*" on Resource: "*"
{ "Effect": "Allow", "Action": "*", "Resource": "*" }One leaked credential with this attached owns the entire account. Every service, every resource, every action — read, write, delete, and admin. This is the most dangerous single statement in IAM and it appears with surprising regularity, usually as a "temporary" fix that was never revisited.
Fix: scope to the specific actions and ARNs the workload actually uses. Even a broad starting point like the example below is orders of magnitude safer:
{ "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::my-bucket/*" }2. iam:PassRole with No Condition
{ "Effect": "Allow", "Action": "iam:PassRole", "Resource": "*" }This is a classic privilege-escalation path. A principal with iam:PassRole on Resource: "*" can pass a more-privileged role to a service they control — a Lambda function, an EC2 instance, an ECS task — and then invoke that service to harvest admin-level credentials. The escalation is indirect enough that it often passes code review.
Fix: restrict the resource to the exact role ARN and add a condition locking the receiving service:
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::123456789012:role/my-lambda-role",
"Condition": { "StringEquals": { "iam:PassedToService": "lambda.amazonaws.com" } }
}3. Public s3:GetObject on a Bucket Policy
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}Principal: "*"means the public internet. This is how data leaks quietly. The bucket does not need to be called "public" — any object in it is readable by anyone with the URL, including unauthenticated requests. Block Public Access settings can override this at the account level, but relying on that control instead of fixing the policy is fragile.
Fix: name the exact principal, or gate with aws:SourceArn or a VPC endpoint condition, and enable Block Public Access at both bucket and account level.
4. Over-broad sts:AssumeRole Trust Policy
A trust policy that allows an entire AWS account (or Principal: "*") to assume a role invites cross-account abuse and the confused-deputy problem. Any principal in that account — including ones created after the trust policy was written — can assume the role. For third-party integrations this becomes a confused-deputy vector: a shared external service can be tricked into acting on behalf of a different customer.
Fix: pin the exact principal ARN and, for third parties, require an ExternalId:
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:role/ci-deployer" },
"Action": "sts:AssumeRole",
"Condition": { "StringEquals": { "sts:ExternalId": "your-unique-id" } }
}5. NotAction Used as a Denylist
{ "Effect": "Allow", "NotAction": "s3:DeleteObject", "Resource": "*" }People read this as "deny delete." It actually allows every action except delete — which is almost everything. NotAction with Effect: Allow is not a denylist; it is a whitelist of everything you did not name. This catches engineers who intend to write a narrow allow policy but reach for NotAction thinking it is the safer option.
Fix: use an explicit Deny statement if you need to block a specific action, or list the allowed actions positively in an Allow statement.
6. kms:* on a Customer-Managed Key
Full KMS permission on a customer-managed key (CMK) means whoever holds it can decrypt, re-encrypt, and re-key your data. It can also disable or schedule deletion of the key — which would make your data permanently unrecoverable. This pattern appears when a developer grants kms:* because they needed kms:Decrypt and wildcarding was faster than looking up the right action name.
Fix: grant only the specific KMS actions the role actually needs. For a workload that reads encrypted data, that is usually just kms:Decrypt and kms:GenerateDataKey:
{
"Effect": "Allow",
"Action": ["kms:Decrypt", "kms:GenerateDataKey"],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/your-key-id"
}7. Missing aws:SourceArn / aws:SourceAccount Guards
When a service principal — SNS, S3, CloudWatch Events, EventBridge — can invoke your Lambda or write to your queue without a source condition, any account's instance of that service can trigger it. This is the confused-deputy problem at the service level: you intended to allow your S3 bucket to trigger your function, but the policy allows any S3 bucket in any account to do the same.
Fix: add Condition with aws:SourceArn (and aws:SourceAccount as a belt-and-suspenders guard) to bind the permission to your own resources:
{
"Effect": "Allow",
"Principal": { "Service": "s3.amazonaws.com" },
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:my-function",
"Condition": {
"ArnLike": { "aws:SourceArn": "arn:aws:s3:::my-bucket" },
"StringEquals": { "aws:SourceAccount": "123456789012" }
}
}Catching These Automatically
Knowing the patterns is half the battle; catching them on every change is the other half. Rule-based scanners catch the obvious cases but miss interaction-level risk — the question is not just "does this policy grant PassRole on *" but "given everything this principal can do, can they use PassRole to reach a privileged identity." That requires reasoning across the full permission set.
Shieldly is AI-Powered security analysis for AWS. Paste an IAM policy, resource policy, or CloudFormation template and the AI explains each risk in plain English and gives you the tightened version. It also runs as a GitHub Action so risky access is flagged in the pull request, before it ships. Input is never logged — cache keys are one-way SHA-256 hashes.
Catch these misconfigurations before they ship
Paste an IAM policy and get AI-Powered analysis in seconds — free, no credit card.
Amazon Web Services (AWS) is a trademark of Amazon.com, Inc. Shieldly is not affiliated with, endorsed by, or sponsored by Amazon Web Services.