All articles

// Knowledge.log — 技術記事

When an IAM permissions boundary cuts identity s3:*

Identity Allow s3:* still fails PutObject when the permissions boundary only allows GetObject on a prefix. User Guide intersection, local JSON, no account.

The PR changes the identity policy on the app-reader role to Effect: Allow, Action: s3:*, Resource: arn:aws:s3:::amzn-s3-demo-bucket/*. The reviewer sees the asterisk, decides the application can now do anything it wants with the bucket's objects, and approves. After the deploy, PutObject still fails. The role has no Deny, and neither does the bucket policy. The role does have a permissions boundary, though. It lives in another stack, nobody opened it, and it only allows s3:GetObject on the app/ prefix.

The diff is generous. The ceiling didn't move.

By the end you'll have two local JSON documents, a broad identity policy and a narrow boundary, plus the rule from the IAM docs that explains why only one of three requests gets through. The sources are the IAM User Guide and the S3 User Guide, checked on 2026-10-22. Nothing was run against an AWS account or the Policy Simulator.

The boundary is a ceiling, not a grant

The permissions boundaries page defines the mechanism like this:

> AWS supports permissions boundaries for IAM entities (users or roles). A permissions boundary is an advanced feature for using a managed policy to set the maximum permissions that an identity-based policy can grant to an IAM entity. An entity's permissions boundary allows it to perform only the actions that are allowed by both its identity-based policies and its permissions boundaries.

That definition has three practical consequences:

  1. Users or roles only. An identity policy can be attached to a group, but a boundary is set on a user or a role. Groups don't have boundaries.
  2. The boundary grants nothing. It sets the maximum that identity policies can grant. The policies and permissions page is explicit: a boundary "defines the maximum permissions that the identity-based policies can grant to an entity, but does not grant permissions".
  3. The result is an intersection. The evaluation logic page says that when identity policies and a boundary are evaluated together, "the resulting permissions are the intersection of the two categories", and adds that "an explicit deny in either of these policies overrides the allow".

One more detail changes how you read the error. In the evaluation order, once the explicit Deny scan is done, AWS requires an Allow from the identity policy and then checks the boundary:

> If the policy that is used to set the permissions boundary does not allow the requested action, then the request is implicitly denied.

That's an implicit deny, a ceiling. Nobody wrote Effect: Deny anywhere. The action just isn't inside what the boundary allows.

The two JSON documents and the table

The placeholders follow AWS documentation style: the role arn:aws:iam::111122223333:role/app-reader and the bucket amzn-s3-demo-bucket. Both documents use "Version": "2012-10-17", the current policy language version. Neither has a Principal. An identity policy can't contain that element, and a boundary is a managed policy with the same shape, used as a ceiling.

The role's identity policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "IdentityAllowS3StarOnDemoBucketObjects",
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
    }
  ]
}

The permissions boundary on the same role:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "BoundaryAllowGetObjectAppPrefixOnly",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket/app/*"
    }
  ]
}

To make the rule visible, a local Python script evaluates three requests against both documents. It only implements the documented subset: the effective result is Allow if both the identity policy and the boundary allow, and an explicit Deny in either one wins. This is not the IAM engine. The script doesn't model SCPs, RCPs, resource policies, session policies, NotAction, or policy variables. The table illustrates the intersection the User Guide describes. It doesn't replace an evaluation in a real account.

RequestIdentityBoundaryEffectiveWhy
s3:GetObject on .../app/file.txtAllowAllowAllowboth match
s3:PutObject on .../app/file.txtAllowimplicit DenyDenyidentity allows, boundary does not
s3:GetObject on .../other/file.txtAllowimplicit DenyDenyoutside prefix

The identity column says Allow on all three rows, because s3:* on bucket/* matches all three object ARNs. If the review stops at that column, it gets the answer wrong two times out of three.

The second row is the PR from the opening: PutObject "allowed" in the role's JSON and an implicit deny at the ceiling. The third row catches anyone who thinks a boundary only restricts actions. Its Resource is a ceiling too, and other/ falls outside it.

Not the bucket policy Deny (and not an SCP)

The piece on Deny in a resource policy covered a different mechanism. There, the identity policy and the bucket policy in the same account combine by union, and an explicit Effect: Deny in the bucket policy overrides the identity Allow. Here, neither document has a Deny. Access fails because the combination is an intersection, and the boundary never said Allow for PutObject. If you open the bucket policy looking for the culprit, you won't find one. It's also different from IfExists in an IAM policy, which was about a missing context key inside a single identity policy.

Two other ceilings tend to come up in the same conversation:

MechanismWhere it livesGrants?How it combines with identity
Permissions boundarymanaged policy on the user or rolenointersection
SCPorganization, OU, or accountnointersection; with a boundary, all three must allow
Session policypassed when assuming the role or requesting federated credentialsnointersection of identity, session policy, and boundary

The boundaries page sums up the combined case: when an SCP, a boundary, and an identity policy all affect the same request, "the request is allowed only if all three policy types allow it". The SCP is the organization's ceiling, attached at a different level and owned by a different team. A session policy only applies to that one temporary session. The boundary goes wherever the role goes.

Minimal template: a Java app role that only reads app/

For an application that should only read objects under app/ and list that prefix, the minimal boundary looks like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowGetObjectOnAppPrefix",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket/app/*"
    },
    {
      "Sid": "AllowListBucketAppPrefix",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket",
      "Condition": {
        "StringLike": {
          "s3:prefix": "app/*"
        }
      }
    }
  ]
}

The two Resource values differ on purpose. In the S3 identity policy examples, object actions (GetObject, PutObject, DeleteObject) use the object ARN, arn:aws:s3:::bucket/prefix/*. s3:ListBucket uses the bucket ARN, with no /*, and scopes the listing with "Condition": { "StringLike": { "s3:prefix": ["${aws:username}/*"] } }. The template swaps the variable for a fixed prefix, app/*. Mixing up the ARNs between the two actions is a classic mistake. Also note that the earlier fixture boundary, which only has GetObject, doesn't allow ListBucket.

Three ways to break the template if you're not careful:

  • The boundary doesn't grant. The role still needs an identity policy that allows the same actions. The simplest approach is to repeat both statements in the identity policy. The fixture's s3:* on bucket/* wouldn't even cover ListBucket, because the bucket ARN doesn't match bucket/*.
  • Set it as the role's permissions boundary. Don't attach it as one more identity policy, and never on a group, since groups don't take boundaries.
  • No Principal. The document has the shape of an identity policy.

Limits of what was shown

The table's results come from the documentation, applied to two JSON documents by a local script. No AWS account, CloudTrail, or Policy Simulator was involved.

The intersection also doesn't explain every access path. According to the policies page, a boundary doesn't limit what a resource-based policy grants, and the User Guide lists exceptions. A same-account resource policy that grants access to an IAM user ARN isn't limited by the boundary's implicit deny. A grant to a role ARN is. So "the boundary always beats the bucket policy" isn't a safe rule. What always holds is the explicit Deny. If the boundary has an Effect: Deny statement that matches the request, it blocks access the same way it would in any other evaluated policy, and the official boundary examples use that kind of statement as a guardrail.

What to do in the next review

Before treating an identity s3:* as production access, open the permissions boundary set on the user or role. Open the actual document, not just the policy the PR changes. The effective result is Allow only where both documents allow and neither denies. If the boundary lives in another stack, it's still part of the diff, even though it doesn't show up in it.

For an application that should only read app/, we recommend putting that ceiling on the role with the template above and keeping an identity policy with the same Allow. When a PutObject fails while the identity "allows everything", the first question should be "does the boundary allow this action on this ARN?", not "who put a Deny on the bucket?". Drop that two-column reading when the boundary has an explicit Deny, when an SCP or session policy is part of the request, or when access comes from a resource policy. In those cases the two-column table isn't enough, and you need to evaluate in the account.

awsiams3

// Continue.training — 次のステップ

Knowledge only counts when it becomes practice.

Go back to the article, run the examples, and share what you learned.

Explore more articles