Home Resume
Home Blog AWS Daily Intelligence #11 - S3 Access Denied Now …
AWS Daily Intelligence AWS

S3 Access Denied Now Names the Policy, Except When It Cannot

The most expensive question in AWS finally has an answer in the error message. Knowing when it does not is what saves you the afternoon.

Verified against current vendor documentation on 14 August 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.
AWS Daily Intelligence #11

Executive summary

Amazon S3 access denied errors now carry the policy type that caused the denial, and for explicit denials, the ARN of the specific policy. The same detail is written to AWS CloudTrail. It applies in all Regions, including GovCloud (US) and China, and there is nothing to enable.

That answers a question that has cost the industry an enormous amount of time: not "am I denied", which you always knew, but "denied by what".

The value is real. The limits are the part worth internalising, because each one returns you to the old debugging problem without saying so:

  • An implicit deny can never carry an ARN. If nothing allowed the action, there is no policy to name. You get the policy type and the phrase because no {type} policy allows the {action} action. This is not a gap AWS can close later — the policy does not exist.
  • Resource-based policies are not on the ARN list. On S3 that means the bucket policy — arguably the most common source of an S3 denial — is the one type that is never named by ARN.
  • When several policy types deny the same request, AWS names only one. Fix the named one and the next one surfaces. A layered denial reveals itself one layer at a time.

And detail is returned only for same-account or same-organization requests. Cross-account calls from outside your organization still return a bare Access Denied, which is deliberate and correct.

What changed

The message format itself is unchanged in shape:

User {user-arn} is not authorized to perform {action} on "{resource-arn}" because {context}

The first three fields have always been there. The {context} field is what gained substance. It now explains which kind of policy denied the request, and in the explicit case, which policy document.

An explicit denial — a Deny statement matched your request:

User: arn:aws:iam::123456789012:role/HR is not authorized to perform: codedeploy:ListDeployments
on resource: arn:aws:codedeploy:us-east-1:123456789012:deploymentgroup:*
with an explicit deny in an identity-based policy: arn:aws:iam::123456789012:policy/HRAccessPolicy

An implicit denial — nothing matched at all:

User: arn:aws:iam::123456789012:role/HR is not authorized to perform: codecommit:ListRepositories
because no identity-based policy allows the codecommit:ListRepositories action

Read those two carefully, because the difference between them is the whole post. The first hands you a document to open. The second hands you a category to go searching in.

The policy ARN is included for service control policies, resource control policies, identity-based policies, session policies and permissions boundaries. The detail is also written to CloudTrail, and enhanced messages cover same-account and same-organization requests in every Region including GovCloud (US) and China.

Why AWS built this

A denial used to be a dead end wearing the costume of an error message. You were told a principal could not perform an action on a resource — all three of which you already knew, since you wrote the call. The one fact you needed, out of the several policy layers that could each have stopped you, was the one fact withheld.

The standard response was a manual audit: check the identity-based policy, then the bucket policy, then the permissions boundary, then the SCP, then whether a session policy narrowed the role, then whether a condition key silently failed to match. On a large organization that is an afternoon, and it is an afternoon spent re-deriving something the authorisation engine knew with certainty at the moment it said no.

What changed is that the engine now tells you part of what it knew. Understanding exactly which part is what makes this useful rather than merely encouraging.

Architecture

Diagram: the anatomy of an enhanced S3 access denied message, the difference between explicit and implicit denial messages, which five policy types can return an ARN, and the three limits that leave you without one

The distinction the whole feature rests on is one every IAM user has met and few have had to name precisely:

Explicit denialImplicit denial
CauseA policy contains a Deny statement for the actionNo applicable Deny and no applicable Allow
Message phrasewith an explicit deny in a {type} policybecause no {type} policy allows the {action} action
Policy ARNYes, for five policy typesNever
What you do nextOpen that document, find the DenyWork out where an Allow should have been

An IAM policy denies by default. A principal is permitted to do nothing until something explicitly allows it, so the overwhelmingly common denial is the implicit one: you forgot an Allow, or wrote one whose resource ARN or condition did not match as intended.

This is why the ARN cannot be added to the implicit case in some future release. There is no document to point at. The denial is the absence of a document, and an absence has no ARN.

The trap: three reasons you still get no ARN

Each of these is documented. None of them is in the announcement.

1. The bucket policy is not on the list. The five types that return an ARN are SCPs, RCPs, identity-based policies, session policies and permissions boundaries. Resource-based policies are absent — and on S3, the resource-based policy is the bucket policy. AWS's own example of an explicit denial by a resource-based policy carries no ARN:

User: arn:aws:iam::123456789012:user/John is not authorized to perform: secretsmanager:GetSecretValue
on resource: arn:aws:secretsmanager:us-east-1:123456789012:secret:*
with an explicit deny in a resource-based policy

For an S3 denial specifically, that is a pointed limitation. Bucket policies are where the deliberate, explicit Deny statements tend to live — deny unencrypted uploads, deny requests not over TLS, deny principals outside an org. You are told a resource-based policy denied you. Which bucket policy, and which statement in it, remains yours to find. In fairness the resource is named in the message, so on S3 the bucket is identifiable; on a bucket with a two-hundred-line policy that still leaves real work.

2. Only one policy type is named, even when several deny. AWS is explicit: if multiple policy types deny an authorization request, only one of them appears in the message. Consider a request denied by both an SCP and a permissions boundary. You are told about one. You remove that Deny, redeploy, retry — and are denied again, by the layer you were never told about. Nothing indicates that the first fix worked. From the outside it looks like the change did not take effect.

This is the failure mode most likely to waste an afternoon despite the improvement, because the message reads as a complete diagnosis when it is a partial one.

3. The count is withheld. If several policies of the same type deny the request, the message does not say how many were evaluated. One ARN does not mean one cause. Removing the named Deny may simply promote the next policy of the same type into the message.

Treat a named policy as the first cause, not the only one. The message answers "what stopped this request" with one true answer, not with all of them. After removing a Deny, always re-run the call and read the new message rather than assuming the path is now clear. The improvement is that each round trip is now seconds instead of an audit — not that there is only one round trip.

The cross-account behaviour is not a trap but is worth stating: requests from outside your organization return a generic Access Denied. That is correct. Telling an unknown caller which of your policies stopped them, and what it is called, would leak your security posture to precisely the party you are refusing.

Business value

The value is time, and it is unevenly distributed across the cases you will actually hit.

DenialBeforeNowImprovement
Explicit, identity-based / SCP / RCP / session / boundaryAudit every layerNamed policy ARNLarge — open the document
Explicit, bucket policyAudit every layerType named, bucket known, statement notModerate
Implicit, any typeAudit every layerType named, no ARNModerate — the search is scoped, not ended
Several types denyingAudit every layerOne type named per attemptIterative — several fast rounds
Cross-account outside the orgGeneric messageGeneric messageNone, by design

The honest summary: the best case went from an afternoon to a click, and the common case — an implicit deny — went from "check six layers" to "check one layer". Narrowing a six-way search to a one-way search is a genuine improvement even though it is not the headline one.

Security considerations

The obvious question is whether returning policy ARNs to a caller leaks information. The boundary AWS drew is the right one, and it is worth understanding rather than just accepting.

Detail is returned only for same-account and same-organization requests. In both cases the caller is already inside your trust boundary; someone who can call your API from within your organization can generally discover your policy names by other means, and withholding the reason mainly obstructs your own engineers. Outside the organization, the caller is untrusted by definition, and a policy ARN would tell them your account structure, your naming conventions and which control blocked them — useful only for probing. Those callers get the bare message.

Two consequences worth planning for:

Error messages become sensitive artefacts inside the account. A denial message may now contain an SCP ARN including your organization ID and policy ID. If your application logs raw AWS error strings and ships them to a third-party log aggregator, a support desk, or a browser console, that detail travels with them. This is not a reason to avoid the feature; it is a reason to check where your error strings end up.

The CloudTrail copy is the security-useful one. Because the context is written to CloudTrail, denials become analysable in aggregate rather than one at a time. A control that is denying far more than expected is a signal you could not previously see without instrumenting every caller.

Cost considerations

There is no charge for the feature and nothing to enable. It changes the content of an error you were already receiving.

Two second-order effects are worth a moment. CloudTrail now carries additional context on denial events, which marginally increases event size; if you route CloudTrail to S3 and query it with Athena, that is a rounding error against the volume, but it is not zero on a noisy estate.

One S3-specific note that is easy to miss and works in your favour: for 403 errors, S3 does not charge the bucket owner when the request originates outside the bucket owner's own account or organization. A bucket being probed from outside does not bill you for the refusals.

The real economics are labour. Engineer hours spent bisecting policy layers were always the cost of a denial, and they were invisible because nobody billed them to the feature that caused them.

Operational considerations

Not every service returns this format. AWS notes that some services do not support the enhanced message format, and that content varies by the service making the authorization request. Do not build a parser that assumes the phrasing is universal across AWS.

Which is to say: do not parse these strings for control flow. The formats are documented for humans troubleshooting, not as a stable contract. Use them to diagnose; use CloudTrail fields if you need something programmatic.

The message names the type, which tells you where to look even without an ARN. This is the part to train a team on. because no identity-based policy allows means stop looking at the bucket policy. with an explicit deny in a service control policy means the fix is not in the account at all and needs whoever owns the organization. Routing the ticket correctly on the first read is most of the saving.

VPC endpoint policies have a known blind spot. If you use a VPC endpoint policy and get a denial that does not appear in CloudTrail at all, the cause may be that the endpoint owner account differs from the calling account or the target role account.

Condition keys remain the hardest case. A policy whose Allow did not match because a condition failed produces an implicit denial — no ARN, and no indication that a condition was the reason. AWS's own warning is worth repeating: condition key names are case-insensitive, so a condition checking foo matches Foo and FOO, and a request carrying key names differing only by case can be denied unexpectedly.

Tradeoffs

GainCost
The authorisation engine now reports part of what it knewOnly part — one type, one ARN, no count
Nothing to enable, every Region, no chargeNothing to opt out of either, if you would rather your error strings stayed terse
Detail flows to CloudTrail for aggregate analysisSlightly larger denial events
Same-org scoping prevents leaking posture outwardCross-account debugging with partners is unchanged
Policy type is always named, even without an ARNEncourages reading the message as complete when it is one of several causes

The one genuine risk in this feature is a human one. A message that confidently names a cause invites you to stop reading after it, and the documentation is clear that it may be one cause among several.

Implementation guidance

1. Read the verb before the noun. The two phrasings mean entirely different work:

If the message saysThen
with an explicit deny in a {type} policy: {arn}Open that policy, find the Deny matching the action
with an explicit deny in a {type} policy (no ARN)Search policies of that type only — typically the bucket policy on S3
because no {type} policy allows the {action} actionNothing denied you. Add or correct an Allow of that type

2. Re-run after every fix, and read the new message. Do not assume the first named cause was the only one. This is cheap now, which is the point.

3. Query the CloudTrail copy rather than collecting anecdotes. Denials are far more informative in aggregate. Which policies deny most often, which principals are repeatedly blocked, whether a newly deployed SCP caused a spike — none of that is visible from a single caller's error.

4. Check where your application sends AWS error strings. They may now contain policy ARNs and organization IDs. Confirm they are not surfacing in client-side responses or third-party log tooling.

5. Do not parse the string in code. The format is documented for troubleshooting and varies by service. Build on CloudTrail fields instead.

6. When the type is a service control policy, route the ticket immediately. No amount of work inside the member account will fix it. The message telling you the type is the whole value in this case, ARN or not.

Best practices

  • Teach the two phrasings. "Explicit deny" versus "no policy allows" is the single most useful distinction a team can internalise here, and it costs one sentence to explain.
  • Treat the named policy as a first cause. Re-test after each change rather than assuming the path is clear.
  • Keep bucket policies short and commented. They are the type that never gets an ARN, so the cost of a sprawling bucket policy is now higher relative to everything else.
  • Alarm on denial patterns in CloudTrail, not on individual denials. A single denial is usually a person learning something; a spike is usually a deployment.
  • Prefer fewer Deny statements spread across fewer layers. Every additional layer that can deny is another round trip the message will surface only one at a time.
  • Suspect conditions when an implicit deny makes no sense. A policy that looks like it allows the action, on a denial with no ARN, usually means a condition did not match.

Who should adopt, who should wait

There is nothing to adopt. It is on, everywhere, at no cost, and it changes only the text of errors you were already getting. The question is not whether to use it but what to change because of it.

Act on it now if you run an organization with SCPs or RCPs, where denials previously required someone with organization-level visibility to diagnose — the type name alone now routes the ticket correctly. Or if you operate a platform team fielding "why can't I access this bucket" requests, where the first response can now be "paste the full error" and frequently end there.

Review something because of it if your applications log raw AWS error strings to places you do not fully control. That detail is richer than it was last week.

Do not expect it to help if your denials are cross-account from outside your organization, or if the bulk of your S3 denials come from bucket policies, where you get the type and the bucket but not the statement.

Key takeaways

  • S3 access denied errors now include the policy type that denied the request, and for explicit denials the ARN of the specific policy. Nothing to enable, no charge, all Regions including GovCloud (US) and China.
  • The ARN is returned for SCPs, resource control policies, identity-based policies, session policies and permissions boundaries.
  • Resource-based policies are not on that list. On S3 that is the bucket policy — the type most likely to carry a deliberate Deny.
  • An implicit denial — because no {type} policy allows the {action} action — can never carry an ARN. Nothing denied you; nothing allowed you. There is no document to name.
  • Because IAM denies by default, the implicit case is the common one. The improvement there is a narrowed search, not an answer.
  • If several policy types deny the same request, AWS names only one. Fix it and the next surfaces, with nothing to indicate the first fix worked.
  • If several policies of the same type deny it, the message does not say how many were evaluated.
  • Detail is limited to same-account and same-organization requests. Cross-account from outside returns a generic message, deliberately.
  • The same context is written to CloudTrail, which is where the durable value is — denials become analysable in aggregate.
  • Do not parse these strings in code. Some services do not use this format, and content varies by service.

Official AWS references

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent