Most AWS security incidents do not start with an exotic exploit. They start with an over-broad policy, a key that was never rotated or a role that trusts more than it should, each created because it was the fastest way to make something work. Here are the IAM mistakes that recur most often, what to do instead, and how to check that the fix held.
1. Wildcards in actions and resources
{ "Effect": "Allow", "Action": "*", "Resource": "*" }
That is administrator access. Less obvious variants are s3:* on every bucket, or ec2:* for a pipeline that only starts and stops instances. Name the actions and narrow the resource:
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::example-bucket/uploads/*"
}
Object-level actions such as s3:GetObject apply to the object ARN (bucket/prefix/*), while s3:ListBucket applies to the bucket ARN - mixing these up is a common reason a "tight" policy fails and someone swaps in a wildcard. IAM Access Analyzer can generate a policy from CloudTrail activity; test it, and don't treat the first working policy as the final one.
2. Long-lived access keys
A leaked key keeps working until someone notices. Prefer temporary credentials:
- Workloads on AWS: instance profiles, ECS task roles, or a Kubernetes service account mapped to a role.
-
CI/CD: OpenID Connect federation (
sts:AssumeRoleWithWebIdentity) - no stored key. - People: federated sign-in through IAM Identity Center.
Audit what's left with aws iam generate-credential-report and aws iam get-credential-report.
3. Loose trust policies
Trusting arn:aws:iam::123456789012:root trusts the whole account. Name the specific role instead, use sts:ExternalId for third parties, and for GitHub Actions OIDC roles always restrict the sub claim:
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
}
}
4. Unrestricted iam:PassRole
iam:PassRole on "Resource": "*" lets a user hand compute a more powerful role than their own. Restrict it to specific role ARNs and add the iam:PassedToService condition. Treat iam:CreatePolicyVersion, iam:AttachUserPolicy and iam:PutRolePolicy the same way.
5. No guardrails above individual policies
Permissions boundaries cap what a role's policies can grant; SCPs cap whole accounts; an explicit deny anywhere wins. Effective permissions are the intersection.
6. Shared users and unused identities
Shared logins destroy auditability. Use last-accessed data or Access Analyzer's unused-access findings to remove old users, roles and keys. The root user: MFA, no access keys, rarely used.
7. Skipping MFA and visibility
Require MFA (passkeys or security keys where you can), keep CloudTrail logs somewhere workload roles can't delete, and review Access Analyzer findings.
A quick audit for this week
- Find policies with
"Action": "*"or"Resource": "*"and justify each. - Generate a credential report; delete or rotate old keys.
- Read the trust policy of every role assumable from outside the account.
- List who holds
iam:PassRoleand IAM-write permissions. - Move one CI pipeline from a stored key to OIDC.
Want to see how well this sticks? I put together a free 10-question AWS skills check - scenario questions with an explanation for every answer, no sign-up: braintally.com/technical-assessments/aws/check
Originally published on BrainTally.
Top comments (0)