Initializing Enclave...

How to Fix IAM Permissions Boundary Conflicts: User Not Authorized to Perform Action

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15 mins

TL;DR

  • What broke: An IAM principal has an identity policy that grants an action, but the attached permissions boundary does not include that action — AWS denies the call because both must allow it.
  • How to fix it: Add the blocked action and resource ARN to the permissions boundary policy attached to the role or user.
  • Quick path: Use our Client-Side Sandbox above to paste your boundary policy and identity policy — it auto-diffs the gap and generates the corrected boundary document locally.

The Incident (What Does the Error Mean?)

Raw error output:

An error occurred (AccessDenied) when calling the PutObject operation:
User: arn:aws:sts::123456789012:assumed-role/app-deploy-role/session
is not authorized to perform: s3:PutObject on resource:
arn:aws:s3:::prod-artifacts-bucket/deploy/*
because no permissions boundary allows the s3:PutObject action

The phrase "because no permissions boundary allows" is the signal. This is not a missing identity policy — it is a permissions boundary gap. The role app-deploy-role has an inline or managed policy granting s3:PutObject, but the boundary policy attached to that role omits it entirely.

Immediate consequence: Every API call gated by the missing boundary action fails with AccessDenied at runtime. CI/CD pipelines stall, Lambda deployments fail silently, and ECS task roles cannot write logs — depending on which action is blocked.


The Attack Vector / Blast Radius

Permissions boundaries exist specifically to constrain privilege escalation. If a developer or automated pipeline created this role with iam:CreateRole + iam:AttachRolePolicy but the org-wide boundary SCP was not updated, you now have a silent least-privilege violation in both directions:

  1. Operational blast radius: Any service relying on this role is dead. If the role is used by a deployment pipeline, every environment that role touches is frozen.
  2. Security risk of the wrong fix: The instinctive wrong fix is attaching AWS:* or *:* to the boundary to "just make it work." That collapses the boundary entirely — the role can now escalate to any permission its identity policy grants, including iam:PassRole, sts:AssumeRole, or ec2:RunInstances. A compromised workload with that role becomes a full account takeover vector.
  3. Audit trail gap: If the boundary was silently too permissive before and someone tightened it without updating identity policies, you may have multiple broken roles across accounts — check CloudTrail for AccessDenied events with the "reason":"PermissionsBoundary" field across all regions.

CloudTrail query to find all boundary-blocked calls in the last 24h:

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AccessDenied \
  --start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ) \
  --query "Events[?contains(CloudTrailEvent, 'PermissionsBoundary')].{Time:EventTime,User:Username,Event:CloudTrailEvent}" \
  --output table

How to Fix It (The Solution)

Basic Fix — Add the Missing Action to the Boundary

Locate the boundary policy ARN attached to the role:

aws iam get-role --role-name app-deploy-role \
  --query 'Role.PermissionsBoundary.PermissionsBoundaryArn' \
  --output text

Then update the boundary policy to include the blocked action:

 {
   "Version": "2012-10-17",
   "Statement": [
     {
       "Sid": "AllowCoreS3",
       "Effect": "Allow",
       "Action": [
-        "s3:GetObject",
-        "s3:ListBucket"
+        "s3:GetObject",
+        "s3:ListBucket",
+        "s3:PutObject"
       ],
       "Resource": [
         "arn:aws:s3:::prod-artifacts-bucket",
-        "arn:aws:s3:::prod-artifacts-bucket/read/*"
+        "arn:aws:s3:::prod-artifacts-bucket/read/*",
+        "arn:aws:s3:::prod-artifacts-bucket/deploy/*"
       ]
     }
   ]
 }

Enterprise Best Practice — Boundary-as-Code with Condition Keys

Never manage boundary policies manually. Define them in Terraform with explicit resource scoping and a PrincipalTag condition to prevent boundary reuse across trust domains:

 resource "aws_iam_policy" "deploy_role_boundary" {
   name = "deploy-role-permissions-boundary"
   policy = jsonencode({
     Version = "2012-10-17"
     Statement = [
       {
         Sid    = "AllowScopedS3"
         Effect = "Allow"
         Action = [
-          "s3:GetObject"
+          "s3:GetObject",
+          "s3:PutObject",
+          "s3:DeleteObject"
         ]
         Resource = [
-          "arn:aws:s3:::prod-artifacts-bucket/*"
+          "arn:aws:s3:::prod-artifacts-bucket/deploy/*",
+          "arn:aws:s3:::prod-artifacts-bucket/read/*"
         ]
+        Condition = {
+          StringEquals = {
+            "aws:ResourceTag/Environment" = "production"
+            "aws:PrincipalTag/BoundaryScope"  = "deploy"
+          }
+        }
       },
       {
         Sid    = "DenyIAMEscalation"
         Effect = "Deny"
         Action = [
           "iam:CreateRole",
           "iam:AttachRolePolicy",
           "iam:PutRolePermissionsBoundary",
+          "iam:DeleteRolePermissionsBoundary",
           "sts:AssumeRole"
         ]
         Resource = "*"
       }
     ]
   })
 }

The DenyIAMEscalation statement is non-negotiable. Without it, any principal operating under this boundary can remove the boundary itself.


💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

1. Checkov — block boundary-less role creation at PR time:

# .checkov.yml
checks:
  - CKV_AWS_274  # Ensure IAM roles have permissions boundaries attached
  - CKV_AWS_275  # Ensure boundary does not allow iam:* wildcard

2. OPA/Conftest policy — enforce boundary attachment on every aws_iam_role resource:

package terraform.aws.iam

deny[msg] {
  resource := input.resource.aws_iam_role[name]
  not resource.config.permissions_boundary
  msg := sprintf("IAM role '%v' is missing a permissions_boundary. Attach a scoped boundary policy.", [name])
}

deny[msg] {
  resource := input.resource.aws_iam_policy[name]
  statement := resource.config.policy.Statement[_]
  statement.Effect == "Allow"
  statement.Action == "*"
  msg := sprintf("Policy '%v' uses wildcard Action in Allow statement — forbidden in boundary policies.", [name])
}

3. GitHub Actions — run boundary drift detection on every merge to main:

- name: Validate IAM Boundaries
  run: |
    checkov -d ./iam --framework terraform \
      --check CKV_AWS_274,CKV_AWS_275 \
      --hard-fail-on HIGH

4. AWS Config Rule — deploy iam-role-managed-policy-check with the managedPolicyArns parameter set to your org-standard boundary ARN. Any role created without it triggers an SNS alert and auto-remediation Lambda within 60 seconds.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →