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:
- 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.
- 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, includingiam:PassRole,sts:AssumeRole, orec2:RunInstances. A compromised workload with that role becomes a full account takeover vector. - 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
AccessDeniedevents 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.