AWS IAM Permission Boundaries and Service Control Policies Architecture

Permission Boundaries Aren't Access Control

This is the first thing people get wrong. A permission boundary doesn't grant permissions — it limits the maximum permissions that an identity-based policy can grant. Think of it as a ceiling, not a door.

If an IAM role has an identity policy that allows s3:*, but a permission boundary that only allows s3:GetObject, the effective permission is s3:GetObject. The boundary clipped the broad policy down.

Where this gets powerful: you attach permission boundaries to roles that developers create. They can write whatever IAM policies they want, but they can't exceed the boundary. This lets platform teams delegate IAM management without losing control.

Permission Boundary Pattern for Developer Teams

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCommonServices",
      "Effect": "Allow",
      "Action": [
        "s3:*",
        "dynamodb:*",
        "sqs:*",
        "sns:*",
        "lambda:*",
        "logs:*",
        "cloudwatch:*",
        "xray:*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyIAMEscalation",
      "Effect": "Deny",
      "Action": [
        "iam:CreateUser",
        "iam:CreateRole",
        "iam:PutRolePolicy",
        "iam:AttachRolePolicy",
        "iam:DeleteRolePermissionsBoundary"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/developer-boundary"
        }
      }
    },
    {
      "Sid": "DenyOrgModification",
      "Effect": "Deny",
      "Action": [
        "organizations:*",
        "account:*"
      ],
      "Resource": "*"
    }
  ]
}

That middle statement is the critical one. It says: developers can create roles and attach policies, but only if those new roles also have the developer-boundary attached. This prevents privilege escalation — they can't create a role that exceeds their own boundary.

Service Control Policies: The Organization-Level Guard

SCPs work differently from permission boundaries. They apply to entire AWS accounts (or OUs) and restrict what IAM entities in those accounts can do, regardless of their IAM policies. Even the root user of a member account can't override an SCP.

SCPs don't grant permissions either. They're a filter. An action must be allowed by the SCP AND the IAM policy for it to work. If the SCP doesn't mention an action, the default depends on your SCP strategy:

  • Allow-list approach: start with an empty SCP, explicitly allow specific services
  • Deny-list approach: start with the FullAWSAccess SCP, add deny statements for what you want to block

Most teams use deny-list because it's less brittle. An allow-list SCP requires updating every time you want to use a new AWS service, and someone always forgets.

Essential SCPs Every Organization Should Have

# Deny leaving the organization
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyLeaveOrg",
    "Effect": "Deny",
    "Action": ["organizations:LeaveOrganization"],
    "Resource": "*"
  }]
}

# Restrict to approved regions
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyUnapprovedRegions",
    "Effect": "Deny",
    "NotAction": [
      "iam:*", "sts:*", "s3:*",
      "cloudfront:*", "route53:*",
      "support:*", "organizations:*"
    ],
    "Resource": "*",
    "Condition": {
      "StringNotEquals": {
        "aws:RequestedRegion": [
          "us-east-1", "us-west-2", "eu-west-1"
        ]
      }
    }
  }]
}

The region restriction SCP excludes global services (IAM, S3 for global operations, CloudFront, Route53) because they technically run in us-east-1, and blocking them would break things in unexpected ways.

Layering SCPs and Permission Boundaries Together

The full picture looks like this: SCPs set the maximum for the entire account. Permission boundaries set the maximum for specific IAM entities within that account. Identity-based policies grant the actual permissions within those two ceilings.

Effective permission = SCP ∩ Permission Boundary ∩ Identity Policy ∩ (Resource Policy, if applicable)

This layering is powerful for multi-team organizations. The platform team manages SCPs at the OU level. Team leads manage permission boundaries for their team's roles. Individual developers manage their own IAM policies within those constraints. Nobody can escalate beyond their layer's ceiling.

Debugging Permission Denials

When something gets denied and you're not sure why, the IAM Policy Simulator doesn't evaluate SCPs. You need to check three places: CloudTrail (which logs the explicit deny source), IAM Access Analyzer (which can simulate the full policy chain), and manual inspection of the SCP → boundary → policy chain.

A practical tip: add unique SIDs to every statement in every policy. When CloudTrail shows a deny, it'll reference the SID, and you can trace it back to the exact statement without guessing.

Practical Deployment Strategy

Rolling out permission boundaries across an existing organization is a multi-phase project. Don't try to apply boundaries to all roles at once.

Start with new roles only. Update your role creation process to attach permission boundaries automatically. Then audit existing roles using IAM Access Analyzer to generate a report of actually-used permissions per role over the last 90 days.

Handling Boundary Exceptions

Some services legitimately need permissions your boundary doesn't allow. Don't widen the boundary for edge cases. Create exception boundaries.

resource "aws_iam_policy" "developer_boundary" {
  name   = "developer-boundary"
  policy = data.aws_iam_policy_document.dev_boundary.json
}

resource "aws_iam_policy" "cicd_boundary" {
  name   = "cicd-boundary"
  policy = data.aws_iam_policy_document.cicd_boundary.json
}

data "aws_iam_policy_document" "cicd_boundary" {
  source_policy_documents = [
    data.aws_iam_policy_document.dev_boundary.json
  ]
  statement {
    actions   = ["iam:PassRole", "ecr:*", "ecs:UpdateService"]
    resources = ["*"]
  }
}

SCP Inheritance and Evaluation Order

SCPs flow down the OU hierarchy. If you attach an SCP to the root OU that denies ec2:RunInstances for m5.24xlarge, that deny applies to every account in the organization. A child OU can add further restrictions but can never grant permissions that a parent SCP denied.

One trap I've seen: teams create an SCP on a child OU that they think grants additional permissions, but SCPs can't grant. If the root OU's SCP already denies an action, a child OU SCP with an Allow for that same action doesn't override the deny. The deny wins. Always.

Testing SCPs Safely

Before applying an SCP to a production OU, test it on a sandbox OU containing a single test account. Attach the SCP, then try every critical action your teams perform. Also test with automation. Your CI/CD pipelines and infrastructure automation have their own IAM roles, and these are often the first things to break.

Cross-Account Role Assumption with Boundaries

In a multi-account organization, teams often need to assume roles in other accounts. The combination of trust policies, permission boundaries, and the assumed role's identity policies creates a complex evaluation chain. Here's how it works in practice.

Account A has a developer role with a permission boundary. That developer assumes a deployment role in Account B. The deployment role has its own identity policy. The effective permissions of the cross-account session are the intersection of: Account B's SCPs, the deployment role's identity policy, and any session policies passed during the AssumeRole call. The permission boundary from Account A doesn't follow the session across account boundaries.

This means you need permission boundaries and SCPs in every account, not just the account where developers log in. A developer with a tight boundary in Account A can assume a role in Account B with no boundary and get full access to whatever the role's identity policy allows. I've seen this gap exploited in penetration tests — the boundary looks tight until you check the cross-account assumptions.

The fix: enforce that all roles created in all accounts have permission boundaries, using the SCP trick from earlier (deny CreateRole unless a boundary is specified). Apply this SCP at the organization root, not just individual OUs.

Auditing and Compliance

IAM Access Analyzer continuously monitors your account for resources shared with external entities. It catches things like S3 buckets with public access, KMS keys shared with other accounts, and IAM roles with trust policies that allow cross-account access. Enable it in every account and route findings to a central security account via EventBridge.

For periodic audits, generate IAM credential reports and access advisor data. The credential report shows every IAM user's password age, MFA status, and access key rotation. Access advisor shows when each permission was last used per service. Permissions unused for 90+ days are candidates for removal — they're attack surface without benefit.

AWS Config rules can continuously evaluate IAM compliance: ensure MFA is enabled for console users, ensure access keys are rotated within 90 days, ensure no inline policies exist on IAM users. Combine these with automatic remediation through Systems Manager to fix non-compliant resources automatically, or at minimum create tickets for the responsible team.