Dev.to Security 🔐 Cybersecurity 👁 0 📖 9 min read

34 Findings in 8 IAM Roles. What You Can't See One Policy at a Time

✓ Human-authored analysis; AI used for formatting and proofreading. A post on r/aws started with this: "im actually losing my mind doing security audits this week. looking at our internal accounts and it feels lik

✓ Human-authored analysis; AI used for formatting and proofreading.

A post on r/aws started with this:

"im actually losing my mind doing security audits this week. looking at our internal accounts and it feels like every single dev just slaps s3:* or literal AdministratorAccess on their roles"

The thread has 50+ comments. Most were about the individual developer's pain. The 10-minute deploy cycle, the AccessDenied with no detail, the "F@#$ It, Chuck It" moment when deadline pressure wins. If that's your problem, you're writing one policy and want to know if it's safe before deploying the companion tutorial covers that with iam-explain, a single-policy analyzer with Z3 formal verification. Write the policy, check it locally, see what's wrong, fix it, check again. Seconds per iteration, no AWS account needed.

This article is about the problem you can't solve one policy at a time.

The OP isn't writing a policy. The OP is auditing internal accounts across an organization and finding the same anti-patterns everywhere. That's a different problem. The question isn't "is this one role safe?". It's "across all the roles in my organization, what compound attack paths exist that no individual role review would catch?"

I took the anti-patterns described in that thread, built each one as a real IAM resource, and ran two tools against them: AWS Access Analyzer and Stave, an open-source configuration verifier that evaluates cross-domain compound properties across organizational snapshots.

The lab

Eight IAM roles, each reproducing a specific anti-pattern from the thread. Three good-practice resources as controls. Observation fixtures were generated from raw IAM policy files using iam-explain --output obs, not hand-authored. The mapping maps each resource back to a practitioner's own words.

R1 — The Slack bot with EC2 full access

"my own senior engineers will deploy a random internal slack bot with full root-level ec2 permissions 'just in case'" — OP

A Lambda role with AmazonEC2FullAccess attached. Stave produced 3 findings: full-access managed policy on a non-administrative role, dangerous action grants (RunInstances, TerminateInstances, AuthorizeSecurityGroupIngress), and Resource: * on sensitive operations.

R2 — The S3 wildcard

"every single dev just slaps s3:*" — OP

A Lambda role with s3:* on Resource: *. Grants not just read and write but s3:DeleteBucket, s3:PutBucketPolicy, and s3:PutLifecycleConfiguration. Stave produced 2 findings.

R3 — AdministratorAccess on Lambda

"literal AdministratorAccess on their roles" — OP

A Lambda execution role with the AdministratorAccess managed policy. A compromised function with this role owns the account. Stave produced 3 findings.

R4 — The wildcard pipeline role with static keys

An IAM user with active access keys assuming a role with Action: *, Resource: *. No OIDC federation with static credentials stored as GitHub secrets. This is the architecture the Trivy supply chain attack exploited in March 2026: a compromised pipeline dependency harvests credentials that are long-lived, usable from anywhere, and grant full account access.

Stave produced 8 findings across the user and role, including the cicd_credential_exfiltration_path chain with the compound finding that connects static credentials to broad permissions and flags the blast radius.

iam-explain would catch the wildcard on either the user or the role individually. It can't see the chain: static keys on the user + admin scope on the role + no OIDC alternative = the Trivy attack shape. That finding exists in the conjunction, not in either policy.

R5 — The foothold triple

An OIDC-federated role with a wildcard subject claim (repo:* where any GitHub repository can assume it) and permissions spanning CloudFormation, IAM, and Secrets Manager. The trust layer looks modern. The permission layer is catastrophic.

Stave produced 7 findings, including the supply_chain_ingress chain with wildcard OIDC trust + broad permissions.

R6 — The propagation triple

A role with correctly scoped OIDC trust (specific repository, specific branch) but overprivileged permissions: ecr:PutImage, secretsmanager:GetSecretValue, s3:PutObject. Each permission is not remarkable alone. The combination is the TeamPCP/Trivy propagation path: push a backdoored container image, read database credentials, modify deployment artifacts.

Stave produced 5 findings, including the cicd_credential_propagation chain.

This is the role that iam-explain would pass with minor warnings. The OIDC trust is scoped, none of the individual permissions are wildcards. Stave catches it because the danger isn't any single permission. It's the compound: ECR push + secrets read + S3 write on a CI/CD role.

R7 — Policy size limit workaround

"The policies have a size limit, and you literally run out of space before you can truly granularly make a policy for a group. So you need to do inverses usually and go very broad instead."

A role with a broad inline "overflow" policy: s3:*, dynamodb:*, sqs:*, sns:*, lambda:* on Resource: *. The practitioner's frustration is real where IAM's policy size limit creates genuine pressure toward broader permissions. Stave produced 2 findings.

R8 — The dev role that leaked to prod

An ECS service role in a production account with AmazonS3FullAccess, AmazonDynamoDBFullAccess, and CloudWatchFullAccess attached. Tagged Environment: production. The permissions were appropriate for a dev sandbox. They were never scoped down when the role moved to production.

Stave produced 4 findings.

The good practices

Three resources that should produce zero findings. A tool that flags good practice alongside bad practice has no signal.

G1 — The scoped Lambda role. The OP described a handover where every Lambda role was scoped to exact DynamoDB ARNs and specific S3 prefixes: "it literally brought a tear to my eye." Reproduced in the lab. Zero findings.

G2 — The properly configured pipeline role. OIDC federation scoped to a specific repo and branch, audience restricted, permissions limited to read-only ECR and S3, session duration at the default. Zero findings.

G3 — The SCP guardrail. Denies iam:CreateUser and AdministratorAccess attachment at the organizational level. Zero findings.

Access Analyzer comparison

Same eight anti-pattern resources. Same account. Three analysis modes.

External access analysis

Access Analyzer's external access analysis flags policies that grant access to external principals with cross-account or public. Every anti-pattern resource produced zero findings. The permissions are dangerous but internal. The Slack bot with EC2 full access isn't externally accessible. The pipeline role with Action: * doesn't grant cross-account access. Overprivilege within an account is outside this analysis mode's scope.

Unused access analysis

Requires 90+ days of CloudTrail data. The lab resources have never been assumed. Every resource was flagged as having unused permissions are trivially true and operationally useless. Even on roles in active use, the question "what hasn't the role done in 90 days?" is different from "what can this role do that it shouldn't?" The first is behavioral observation. The second is a structural safety property.

CheckAccessNotGranted

You pass in a specific action, it tells you whether the policy grants it. For R6, you'd need to separately check ecr:PutImage, secretsmanager:GetSecretValue, and s3:PutObject. Three API calls, three individual answers, no indication that the combination is the finding. You have to know what to look for. Stave ran 13 controls simultaneously without being told what to look for.

Compound chains

Access Analyzer evaluates one policy at a time. It has no compound analysis.

R4 fired cicd_credential_exfiltration_path which is the Trivy attack shape. R5 fired supply_chain_ingress. R6 fired cicd_credential_propagation. These findings are invisible to any tool that evaluates one policy or one action at a time, because the danger is the conjunction, not any individual permission.

Why this gap exists

AWS built its automated reasoning tools such as Zelkova, Tiros, the engines behind Access Analyzer to prove that AWS's side of the shared responsibility boundary operates correctly. The CAV 2019 paper describes Tiros generating PCI DSS compliance evidence for AWS's own services. Zelkova proves that S3 Block Public Access actually blocks public access. These tools prove that AWS's infrastructure does what AWS says it does.

AWS then extracted a subset and made it available to customers, charging for some of it. Access Analyzer unused access analysis costs $0.20 per analyzer per month. Network Access Analyzer charges $0.002 per ENI evaluated. The customer gets tools shaped by AWS's own verification needs. They answer the questions AWS already answered internally. "Is this resource externally accessible?" is a question about the perimeter of the shared responsibility boundary. "What has this role used in the last 90 days?" is a behavioral observation that doesn't require understanding the customer's architecture.

The structural question: does the combination of IAM policies across multiple roles create a compound attack path? lives on the customer's side of the boundary. The tools work exactly as designed. The gap between what they answer and what practitioners need answered is structural.

The results

Resource Anti-pattern Access Analyzer Stave Chains
R1 EC2 full access on Slack bot 0 3
R2 s3:* with Resource: * 0 2
R3 AdministratorAccess on Lambda 0 3
R4 Full wildcard + static keys 0 8 cicd_credential_exfiltration_path
R5 Wildcard OIDC + foothold triple 0 7 supply_chain_ingress
R6 Scoped OIDC + propagation triple 0 5 cicd_credential_propagation
R7 Policy size limit workaround 0 2
R8 Dev full-access in prod 0 4
G1 Scoped Lambda role 0 0
G2 Scoped OIDC pipeline role 0 0
G3 SCP guardrail 0 0
Total 0 34 3

What the compound findings change

The first six findings (R1–R3, R7–R8) are things iam-explain also catches such as wildcard actions, full-access managed policies, resource wildcards. A single-policy check finds them. The organizational audit finds them too, just across more roles at once. The value is scale, not kind.

R4, R5, and R6 are different. The three chain findings — cicd_credential_exfiltration_path, supply_chain_ingress, cicd_credential_propagation connect individual permissions into attack shapes that match real incidents. The Trivy supply chain attack (March 2026) exploited the R4 shape: static credentials in a pipeline with broad permissions. LiteLLM was compromised because a stolen PyPI token propagated through the R6 shape: ECR push + secrets access + artifact write on a pipeline role.

These findings don't exist in any individual policy. They exist in the conjunction such as the combination of trust configuration, permission scope, and credential type across the role's full context. A tool that evaluates one policy at a time, whether it's iam-explain, Access Analyzer, or a manual review, can see each piece. It can't see the shape they form together.

That's the boundary between the two tools. iam-explain gives the individual developer a fast feedback loop on the policy they're writing right now the tutorial shows how. Stave gives the security engineer running the OP's audit a view of the organizational compound risk that no per-policy tool can produce.

In practice, most of these roles are defined in Terraform. iam-explain --terraform reads terraform show -json output directly. It extracts every IAM role in the plan, groups inline policies and managed policy attachments with their parent role, and walks into modules. The developer catches R1–R3 individually before terraform apply. The security engineer feeds the same plan through --terraform --output obs to Stave and catches R4–R6's compound chains. Same Terraform, two scopes.

34 findings. 3 compound chains. 0 false positives on the good practices. Every finding traces back to a practitioner's own words describing the problem they live with every day.

The lab is reproducible. Raw policy files, Terraform definitions, and evaluation commands are in the Stave repository:

# From raw policy files
for policy in labs/iam/policies/R*.json; do
  iam-explain "$policy" --output obs > obs/$(basename "$policy")
done
stave apply --observations obs/ --format table

# From Terraform — same results, one command
terraform show -json plan.out \
  | iam-explain --terraform - --output obs > obs/plan.json
stave apply --observations obs/ --format table

For single-policy analysis, see iam-explain. The tutorial covers --terraform with a before/after CI/CD role example.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.