AWS IAM Zero Trust

Zero Trust in Practice: IAM Policies That Actually Work

The phrase “least privilege” appears in every AWS security guide ever written, yet most of the IAM policies I review in real environments still look like this:

{
  "Effect": "Allow",
  "Action": "s3:*",
  "Resource": "*"
}

This is not least privilege. This is lazy privilege dressed up in IAM syntax. Let’s look at why it keeps happening and — more usefully — how to catch and fix every class of violation before it reaches production.

Why IAM Gets Misconfigured

Overly permissive IAM is rarely malicious. It emerges from three recurring pressures:

  • Speed: Adding * makes the permission error go away immediately.
  • Uncertainty: Developers don’t always know which exact actions an SDK call translates to.
  • Copy-paste culture: Stack Overflow answers and vendor quick-start guides routinely use * because it always works, and the author doesn’t bear the security cost.

The result is a gradual accumulation of over-broad policies across every account you’ve ever touched.

The Five-Point Audit Checklist

1. Wildcard Actions

Look for any Action value ending in :* or literally "*". Each one should be replaced with the exact list of actions the role actually calls. Tools like iamlive can intercept SDK calls in a sandbox and generate a precise action list automatically.

Common offenders:

# What you see in the wild:
"Action": ["ec2:*", "s3:*", "iam:*"]

# What you want instead — example for a deployment role:
"Action": [
  "ec2:DescribeInstances",
  "ec2:DescribeSecurityGroups",
  "s3:GetObject",
  "s3:PutObject",
  "s3:DeleteObject"
]

2. Missing Resource Constraints

"Resource": "*" means the permission applies to every resource in every region in the account. For actions that support resource-level permissions (most of them do), always constrain to the specific ARN or an ARN pattern.

# Bad — full account-wide access:
"Resource": "*"

# Good — scoped to a specific bucket and its objects:
"Resource": [
  "arn:aws:s3:::my-app-prod-artifacts",
  "arn:aws:s3:::my-app-prod-artifacts/*"
]

Some actions genuinely don’t support resource-level permissions (a number of EC2 Describe calls, for example). In those cases, * is unavoidable — but you should have a comment in your IaC explaining why, so future auditors don’t flag it blindly.

3. Missing Condition Keys

Conditions are the most underused feature in IAM. They let you restrict permissions by IP range, VPC, MFA status, time of day, resource tags, and more. A policy without conditions is a policy that works the same way from a developer’s laptop and from a compromised Lambda function exfiltrating data to a foreign IP.

The condition keys I add to almost every production policy:

"Condition": {
  "StringEquals": {
    "aws:RequestedRegion": ["eu-west-1"],
    "aws:PrincipalTag/Environment": "prod"
  },
  "Bool": {
    "aws:SecureTransport": "true"
  }
}

aws:SecureTransport: true on S3 policies prevents HTTP access. aws:RequestedRegion stops a compromised role from spinning up resources in us-east-1 while your compliance boundary requires eu-west-1.

4. Overly Permissive IAM Actions on IAM

Any role that can call iam:CreateRole, iam:AttachRolePolicy, or iam:PutRolePolicy can escalate its own privileges. A CI/CD pipeline that can create IAM roles is effectively an admin. Review every policy that touches iam:* actions and ask whether it genuinely needs the ability to create or modify permissions.

A deploy role that only needs to push to ECR and update an ECS service should never need any IAM permissions. If your deployment pipeline is asking for IAM actions, something has gone wrong in the design.

5. PassRole Overreach

iam:PassRole is sneaky. It looks harmless but it allows a principal to attach any role it can pass to an AWS service. A Lambda function with iam:PassRole: * on Resource: * can pass an admin role to a new EC2 instance and achieve full privilege escalation.

Always scope PassRole to the specific role ARN pattern you intend to pass:

{
  "Effect": "Allow",
  "Action": "iam:PassRole",
  "Resource": "arn:aws:iam::123456789012:role/app-execution-role-*",
  "Condition": {
    "StringEquals": {
      "iam:PassedToService": "ecs-tasks.amazonaws.com"
    }
  }
}

Automating the Audit

Manual review doesn’t scale. Here’s the toolchain I run on every policy, both in CI and as a scheduled scan:

  • AWS IAM Access Analyzer — catches external access, unused permissions, and provides policy validation findings directly in the console and via API.
  • Checkov — static analysis of Terraform and CloudFormation. The CKV_AWS_* IAM rules cover most of the checklist above.
  • Parliament (Duo Security) — lints IAM policies for invalid actions, missing resource constraints, and logical issues. Runs in seconds on a policy JSON file.
  • iamlive — generates a policy from actual API calls in a sandbox. Use this when you don’t know what actions an application actually needs.

My CI pipeline runs Checkov and Parliament on every pull request that touches .tf or policy.json files. IAM Access Analyzer runs nightly on all active accounts and pushes findings to a Security Hub aggregator.

The Mindset Shift

Zero trust isn’t a product you buy. It’s the operational discipline of treating every principal — human, machine, or service — as if it might be compromised, and building permissions so that a compromised credential causes the minimum possible damage.

For IAM, that means: precise actions, precise resources, conditions everywhere they’re supported, and automated enforcement so the policy doesn’t drift back to s3:* the next time someone is in a hurry.