All articles

// Knowledge.log — 技術記事

IAM resource-policy Deny: the identity Allow that never takes effect

An identity Allow for s3:GetObject does not win if the bucket policy has Deny on the same ARN. Three local JSON files and the User Guide, no account.

A PR adds one statement to the identity policy of the app-reader user: Effect: Allow, s3:GetObject, arn:aws:s3:::amzn-s3-demo-bucket/*. The reviewer reads it, checks the ARN and approves. Nobody opens the bucket policy. It isn't in the diff and usually lives in another repository. That bucket policy already has a Deny for this principal, this action and this ARN.

So the merge "proved" that GetObject was allowed. Meanwhile the Deny is still on the bucket, and the access hasn't changed at all.

By the end you'll have a three-row table that pairs an identity policy with a bucket policy. It shows the one case where the identity Allow grants access and the two where it does nothing. The sources are the IAM User Guide and the S3 User Guide, retrieved on 6 October 2026, plus three pairs of local JSON files. Nothing ran against an AWS account or the Policy Simulator. The results in the table are what the documentation says to expect.

The earlier post on IfExists in IAM policies dealt with a missing context key inside a single identity policy. This one is a different problem. Here two policies are evaluated on the same request, one on the identity and one on the resource, and what matters is the Effect in each. None of the JSON here has a Condition.

The evaluation rule, stated once

Every request starts out implicitly denied. The only exception is the account's root user. After that, evaluation follows three steps:

  1. If any applicable policy has an explicit Deny that matches the request, the final decision is Deny.
  2. If there's no Deny, an Allow from the identity policy or the resource policy is enough within the same account, for most resources.
  3. If there's neither a Deny nor an Allow, the default Deny stands.

The policy evaluation logic page describes the result as the union of the permissions from both policy types. Then it adds the sentence that matters here:

> An explicit deny in either of these policies overrides the allow.

The "for most resources" part is in the docs too. The deny vs. allow page says:

> For most resources, you only need an explicit Allow for the principal in either an identity-based policy or a resource-based policy to grant access. IAM role trust policies and KMS key policies are exceptions to this logic.

S3 is not on that list of exceptions. A bucket policy is a resource-based policy, so the general rule applies to it.

All the JSON below uses "Version": "2012-10-17". That is still the current version of the policy language. The only other documented value is the legacy 2008-10-17.

The three JSON pairs

The placeholders are the user arn:aws:iam::111122223333:user/app-reader and the bucket amzn-s3-demo-bucket. The action is s3:GetObject, which acts on objects, so Resource points at the objects (/*) and not at the bucket itself.

One structural difference holds in every case. The identity policy has no Principal. The element isn't allowed there, because the principal is implicitly the identity the policy is attached to. A bucket policy must declare Principal. When you put the two documents side by side, that field tells you which one is which.

Case A: Allow on the identity, Deny in the bucket policy

Identity policy:

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

Bucket policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ResourceDenyGetObject",
      "Effect": "Deny",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:user/app-reader"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
    }
  ]
}

This is the PR from the opening. The identity Allow is correct and it matches the request. The bucket policy's Deny also matches: same principal, same action, same ARN. Under the rule, the explicit Deny wins and the Allow never comes into play. The reviewer approved a statement that is perfectly well written and has no effect.

Case B: Deny on the identity, Allow in the bucket policy

Identity policy:

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

Bucket policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ResourceAllowGetObject",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:user/app-reader"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
    }
  ]
}

This is case A in reverse. Now it's the bucket owner who approves without checking the other side. They grant app-reader access without knowing that the user's identity policy already denies the same action. The outcome is the same, because the rule doesn't care which side the Deny comes from. One explicit Deny in any applicable policy is enough.

Case C: Allow on the identity only

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

Here there's either no bucket policy, or there is one but none of its statements match this request. There's no Deny anywhere and there's an Allow on one side. Within the same account, that's enough. The evaluation basics page says so directly:

> If either the identity-based policy or the resource-based policy within the same account allows the request and the other doesn't, the request is still allowed.

Case C is the only one where the reviewer's quick reading would be right. Even then it would be right by luck, because they didn't open the bucket policy here either.

Expected result in the same account

CaseIdentity policyBucket policyExpected result (same account)
AAllowDeny on the same ARN and principalDenied: the explicit Deny wins
BDenyAllow on the same ARN and principalDenied: the explicit Deny wins
CAllownone (or nothing that matches)Allowed: an Allow on one side is enough

Two of the three rows deny, and you can't get either of those answers from the identity policy alone.

Case C changes when the principal and the bucket are in different accounts. AWS then evaluates the request in both accounts, and the cross-account evaluation page is explicit:

> The request is allowed only if both evaluations return a decision of Allow.

So an identity Allow in the caller's account doesn't grant GetObject unless the bucket policy in the account that owns the resource also allows it. This contrast comes from the documentation only. There's no fourth fixture and no AssumeRole was run.

Where the review goes wrong

The main mistake is treating the identity Allow as sufficient. Sometimes it's necessary, as in the cross-account case. It's sufficient only when no other applicable policy has a matching Deny. "Sufficient" is a claim about every policy on the request, and the diff showed only one of them.

The second mistake is subtler: treating a statement that doesn't apply as if it were an Effect: Deny. Say the bucket policy has an Allow for a different prefix or a different principal. That statement doesn't match this request, so for this request it's just an implicit deny. It doesn't cancel the identity Allow. A matching Deny does. When you open the bucket policy, don't ask "does it grant me access?". Ask "does any statement with Effect: Deny match this principal, this action and this ARN?"

The third mistake is borrowing intuition from other services. KMS key policies and IAM role trust policies are the documented exceptions to "an Allow on one side is enough". They aren't a model for reasoning about S3. It doesn't work the other way either: what holds for the bucket doesn't explain how they behave.

What this table doesn't cover

The table only pairs the identity policy's Effect with the bucket policy's, in a single account, with no Condition. Real access goes through other layers that can deny or narrow what the table marks as allowed: SCPs, RCPs, permissions boundaries, session policies, Block Public Access, and ACLs (along with the bucket's Object Ownership setting). None of those is modeled here. The results also weren't confirmed in an account or in the Policy Simulator. They are the User Guide's rules applied to three JSON pairs.

What to do in review

Treat the bucket policy as part of the diff, even when it doesn't show up there. Open both documents: the identity policy the PR changes, and the bucket policy that's actually on the bucket. A stale copy in another repository doesn't count. First look for Effect: Deny statements that match the request's principal, action and ARN. Only after that is it worth reading the Allows.

If a Deny like that exists, a PR that only adds an identity Allow doesn't grant access, and DevDojo would reject the merge. The conversation has to move to whoever owns the bucket policy. Either the Deny is wrong and gets fixed there, or the Deny is right and the access request shouldn't go through. No Allow on the other side gets around it.

DevDojo wouldn't merge an S3 access change after reading only the identity policy. The rule matters less when the change doesn't depend on any bucket policy at all, but you only know that after opening the bucket policy. In a cross-account setup the bar is higher still, because both sides need an Allow.

Next step

Next time you review an s3:GetObject change, open the identity policy and the bucket policy side by side in the same editor and search for Effect: Deny before you read any Allow. Then match the pair to one of the three rows in the table. If it doesn't fit any of them, the answer is in one of the layers the table leaves out, and that's when an evaluation in the account is worth doing.

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