← back to blog
security·Sep 8, 2025·10 min read

AWS IAM Mastery: Least Privilege at Scale with Permission Boundaries

Building a scalable IAM strategy — permission boundaries for safe delegation, ABAC with tags, SCPs as guardrails, IAM Access Analyzer for drift detection, and eliminating static credentials completely.

SJ
Sabin Joshi
DevOps Engineer
#aws#iam#security#permission-boundaries#abac#scp#access-analyzer

How IAM Goes Wrong

IAM failures follow a predictable pattern: start with AdministratorAccess for convenience, add roles as complexity grows, end up with hundreds of roles — some with wildcard permissions nobody has audited. A breach of any one could mean full account compromise.

The fix isn't manual audits. It's building a system where over-permissioning is structurally hard and violations surface automatically.

Permission Boundaries

Boundaries let you safely delegate IAM management to developers without enabling privilege escalation. The boundary sets the maximum permissions a role can have — even if the role policy grants more, the boundary limits what's effective.

How Permission Boundaries Constrain Effective Permissions
{arr('a','#555')} Role Policy What dev granted • s3:* • ec2:* • rds:* • iam:* • lambda:* • kms:* Permission Boundary Max ops team allows • s3:GetObject • s3:PutObject • ec2:Describe* • rds:* ✗ • iam:* ✗ • lambda:* • kms:Decrypt Effective Permissions intersection of both ✓ s3:GetObject ✓ s3:PutObject ✓ ec2:Describe* ✗ rds:* (boundary) ✗ iam:* (boundary) ✓ lambda:* ✓ kms:Decrypt = Effective = Role Policy ∩ Permission Boundary ∩ SCPs
resource "aws_iam_policy" "developer_boundary" {
  name = "DeveloperPermissionBoundary"
  policy = jsonencode({
    Statement = [
      { Effect = "Allow", Action = ["s3:*","lambda:*","logs:*"], Resource = "*" },
      {
        # Deny creating roles WITHOUT this boundary — blocks privilege escalation
        Effect    = "Deny"
        Action    = ["iam:CreateRole","iam:PutRolePolicy"]
        Resource  = "*"
        Condition = { StringNotEquals = {
          "iam:PermissionsBoundary" = "arn:aws:iam::ACCT:policy/DeveloperPermissionBoundary"
        }}
      }
    ]
  })
}

Attribute-Based Access Control (ABAC)

ABAC replaces hundreds of role-per-team policies with a single policy that uses tags. A developer with team=payments on their role automatically gets access to all resources tagged team=payments.

# One policy rule — replaces 50+ team-specific policies
{
  "Effect": "Allow",
  "Action": ["s3:*", "dynamodb:*", "secretsmanager:GetSecretValue"],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "aws:ResourceTag/team": "${aws:PrincipalTag/team}",
      "aws:ResourceTag/env": "${aws:PrincipalTag/env}"
    }
  }
}

IAM Access Analyzer

Access Analyzer continuously scans your IAM policies and resource policies for external access paths you didn't intend. It automatically creates findings for S3 buckets, KMS keys, Lambda functions, and IAM roles that are accessible from outside your account or organization. We integrate findings into our security dashboard and treat them as P1 — resolve within 24 hours.

⚠️Run Access Analyzer's policy validation on every new IAM policy in your CI pipeline. It catches common mistakes (wildcards in resource ARNs, missing condition keys) before they reach production.

Eliminating Static Credentials

Every static AWS access key is a ticking time bomb. Our path to zero static credentials: CI/CD uses OIDC federation (GitHub Actions → IAM role), applications use EC2 instance profiles or ECS task roles, on-prem workloads use IAM Roles Anywhere, and developers use AWS SSO with short-lived session tokens. Zero aws_access_key_id in any config file, anywhere.