AWS Identity and Access Management (IAM) is the foundation of every secure AWS environment. It’s also one of the most common sources of both production outages and security vulnerabilities. After five years of working with AWS at scale, I’ve landed on a set of patterns that make IAM both secure and liveable.

The Problem with “Permissive First” IAM

Most teams start with overly permissive IAM policies — "Action": "*", "Resource": "*" — because it unblocks immediate work. The problem is these policies calcify. They never get tightened. Months later, you’re doing a security audit and discovering that your CI/CD service account has s3:DeleteBucket on every bucket in the account.

The real cost isn’t the security risk alone. It’s that you lose the ability to reason about blast radius. If that service account is compromised, what can an attacker actually do?

Start with IAM Access Analyzer

Before writing any policies, run IAM Access Analyzer across your account. It tells you which resources are accessible from outside your account (S3 buckets, KMS keys, Lambda functions) and flags overly permissive policies.

# Enable Access Analyzer via CLI
aws accessanalyzer create-analyzer \
  --analyzer-name my-org-analyzer \
  --type ORGANIZATION

This gives you a baseline. You know what’s exposed before you touch anything.

Use Service Control Policies as a Safety Net

In a multi-account AWS Organization, SCPs are your insurance policy. They’re permissions guardrails at the organization level — even if someone grants AdministratorAccess in an account, the SCP can block specific high-risk actions.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRootUsage",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:root"
        }
      }
    },
    {
      "Sid": "DenyLeaveOrganization",
      "Effect": "Deny",
      "Action": [
        "organizations:LeaveOrganization"
      ],
      "Resource": "*"
    }
  ]
}

These two SCP statements alone eliminate two major threat vectors: root account usage and account removal from your organization.

The Role Pattern That Actually Scales

The pattern I’ve found most maintainable at scale is: one IAM role per workload, per environment.

app-name-prod-role
app-name-staging-role
app-name-dev-role

Each role has exactly the permissions that workload needs. Role names follow a convention that makes audit trails readable. Trust policies scope who can assume the role.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "aws:SourceAccount": "123456789012"
        }
      }
    }
  ]
}

The SourceAccount condition in the trust policy prevents confused deputy attacks — a nuance that many teams miss.

Generating Least-Privilege Policies with CloudTrail

The least-privilege trap is that you don’t know what permissions a workload needs until it runs. AWS has a solution: CloudTrail + IAM Access Analyzer policy generation.

Run your application with a permissive policy in a non-prod environment, then use this CLI command:

aws accessanalyzer start-policy-generation \
  --policy-generation-details "principalArn=arn:aws:iam::ACCOUNT_ID:role/my-role" \
  --cloud-trail-details "trailArns=[arn:aws:cloudtrail:us-east-1:ACCOUNT_ID:trail/my-trail],startTime=2026-01-01T00:00:00Z,endTime=2026-02-01T00:00:00Z"

This generates a policy based on what your role actually called, not what you think it called. It’s not perfect — you need enough CloudTrail coverage — but it’s a much better starting point than guessing.

Key Takeaways

  • Start with IAM Access Analyzer before touching any policy
  • SCPs are non-negotiable in multi-account environments
  • One role per workload per environment is the maintainable pattern
  • Use CloudTrail-based policy generation for initial least-privilege scoping
  • Review and tighten policies quarterly — they drift otherwise

IAM is one of those services where a small investment in getting it right early pays dividends for years. The alternative is a security incident or an audit where you can’t explain why your Lambda has S3 admin access.