I Checked the Conditions Under Which the Policy ARN Is Displayed in an S3 403 Error

I Checked the Conditions Under Which the Policy ARN Is Displayed in an S3 403 Error

Amazon S3 403 Access Denied errors now show the ARN of the policy that explicitly denied access. This article checks when the ARN appears for SCPs, RCPs, identity-based and session policies, and permissions boundaries.
2026.08.14

This page has been translated by machine translation. View original

Introduction

On August 13, 2026, Amazon S3 was updated so that 403 Access Denied error messages include the ARN of the policy that denied access.

https://aws.amazon.com/about-aws/whats-new/2026/08/s3-additional-policy-details-access-denied-error-messages/

Iwasa's article introduces the behavior for explicit denies from identity-based policies, permissions boundaries, SCPs (service control policies), and RCPs (resource control policies).

In this article, in addition to those, I covered session policies, inline policies, and bucket policies, and checked the conditions under which the policy ARN is displayed. I also verified whether the ARN is exposed to unauthenticated requests from outside the organization.

What I verified

I first list the policy types for which the ARN is displayed and the cases where it is not, and then look at the conditions individually.

What is returned for each policy type

For each policy type that can produce an explicit Deny, I denied s3:GetObject and compared the error messages returned.

Policy type How it was specified Policy ARN Notes
SCP Applied from the management account in Organizations ○ -
RCP Applied from the management account in Organizations ○ -
Identity-based policy Customer managed policy attached to a role ○ -
Session policy Managed policy specified with --policy-arns ○ -
Permissions boundary Customer managed policy set as the boundary ○ -
Session policy (inline) Inline JSON specified with --policy × Type only
Identity-based policy (inline) Set inline with put-role-policy × Type only
Bucket policy Set directly on the bucket × Type only
Implicit Deny (for reference) No Allow exists × Denial reason only
Unauthenticated request from outside the organization (for reference) Unsigned HTTP request × Access Denied only

The "Policy ARN" column indicates whether the error message includes the ARN of the policy that denied access.

I ran the verification against s3:GetObject in a single account within the same organization. The example in the S3 User Guide uses the format , with policy ARN: <ARN>, but all the messages I actually observed used the format : <ARN>.

I used a separate target bucket for each case so that there was only one cause of denial. I used the same command to retrieve the object in every case and only swapped the bucket name. Account IDs, role names, bucket names, and policy IDs have been replaced with example values, so even the same ExampleRole may refer to a different role depending on the case.

aws s3api get-object \
  --bucket <バケット名> \
  --key test.txt \
  /tmp/out.txt \
  --region ap-northeast-1

SCP

I created the following SCP in the management account and attached it directly to the member account.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyS3GetObject",
      "Effect": "Deny",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket-scp/*"
    }
  ]
}
aws organizations attach-policy \
  --policy-id p-examplescpid \
  --target-id 123456789012
An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::123456789012:assumed-role/ExampleRole/ExampleSession is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::amzn-s3-demo-bucket-scp/test.txt" with an explicit deny in a service control policy: arn:aws:organizations::111122223333:policy/o-exampleorgid/service_control_policy/p-examplescpid

RCP

Unlike an SCP, an RCP requires Principal to be specified. I created it in the management account and attached it directly to the member account.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyS3GetObject",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket-rcp/*"
    }
  ]
}
aws organizations attach-policy \
  --policy-id p-examplercpid \
  --target-id 123456789012
An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::123456789012:assumed-role/ExampleRole/ExampleSession is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::amzn-s3-demo-bucket-rcp/test.txt" with an explicit deny in a resource control policy: arn:aws:organizations::111122223333:policy/o-exampleorgid/resource_control_policy/p-examplercpid

Identity-based policy

I created a customer managed policy named DenyS3GetObjectIdentity and attached it to the role used for execution.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyS3GetObject",
      "Effect": "Deny",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket-identity/*"
    }
  ]
}
aws iam attach-role-policy \
  --role-name ExampleRole \
  --policy-arn arn:aws:iam::123456789012:policy/DenyS3GetObjectIdentity
An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::123456789012:assumed-role/ExampleRole/ExampleSession is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::amzn-s3-demo-bucket-identity/test.txt" with an explicit deny in an identity-based policy: arn:aws:iam::123456789012:policy/DenyS3GetObjectIdentity

Session policy

I created DenyS3GetObjectSession as a customer managed policy and specified it with --policy-arns in assume-role. The effective permissions of a session policy are the intersection with the role's policies, so I included an Allow as well so that resources other than the target bucket would not be implicitly denied.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAll",
      "Effect": "Allow",
      "Action": "*",
      "Resource": "*"
    },
    {
      "Sid": "DenyS3GetObject",
      "Effect": "Deny",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket-session/*"
    }
  ]
}
aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/ExampleRole \
  --role-session-name ExampleSession \
  --policy-arns '[{"arn":"arn:aws:iam::123456789012:policy/DenyS3GetObjectSession"}]'
An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::123456789012:assumed-role/ExampleRole/ExampleSession is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::amzn-s3-demo-bucket-session/test.txt" with an explicit deny in a session policy: arn:aws:iam::123456789012:policy/DenyS3GetObjectSession

Session policies are a type that the S3 User Guide lists as eligible for ARN display, and the behavior matched the documentation.

Permissions boundary

I created DenyS3GetObjectBoundary as a customer managed policy and set it as the permissions boundary when creating the role. I granted S3 read permissions on the role side so that the boundary would be the only cause of denial.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAll",
      "Effect": "Allow",
      "Action": "*",
      "Resource": "*"
    },
    {
      "Sid": "DenyS3GetObject",
      "Effect": "Deny",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket-boundary/*"
    }
  ]
}
aws iam create-role \
  --role-name ExampleRole \
  --permissions-boundary arn:aws:iam::123456789012:policy/DenyS3GetObjectBoundary \
  --assume-role-policy-document file://trust-policy.json
An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::123456789012:assumed-role/ExampleRole/ExampleSession is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::amzn-s3-demo-bucket-boundary/test.txt" with an explicit deny in a permissions boundary: arn:aws:iam::123456789012:policy/DenyS3GetObjectBoundary

In every case, only the policy ARN was returned. Which statement matched and what the policy body contains are not included.

The cases where no ARN is returned fall into two groups: types that do not have an ARN in the first place, such as bucket policies, and types that do have an ARN but were specified inline. Both are covered in the next section.

The condition for ARN display is whether the policy has an ARN

Even for the same policy type, the display changed depending on whether it was specified as a managed policy with an ARN or written inline.

First, the session policy. I passed the same policy as in the previous section as inline JSON with --policy instead of --policy-arns.

aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/ExampleRole \
  --role-session-name ExampleSession \
  --policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"},{"Effect":"Deny","Action":"s3:GetObject","Resource":"arn:aws:s3:::amzn-s3-demo-bucket-session/*"}]}'
An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::123456789012:assumed-role/ExampleRole/ExampleSession is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::amzn-s3-demo-bucket-session/test.txt" with an explicit deny in a session policy

The message ended with session policy, and no ARN was appended.

The same applies to identity-based policies. I set it as an inline policy on the role with put-role-policy.

aws iam put-role-policy \
  --role-name ExampleRole \
  --policy-name DenyS3GetObjectInline \
  --policy-document '{"Version":"2012-10-17","Statement":[{"Sid":"DenyS3GetObject","Effect":"Deny","Action":"s3:GetObject","Resource":"arn:aws:s3:::amzn-s3-demo-bucket-inline/*"}]}'
An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::123456789012:assumed-role/ExampleRole/ExampleSession is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::amzn-s3-demo-bucket-inline/test.txt" with an explicit deny in an identity-based policy

Bucket policies also have no ARN, so only the type is returned. I set the following policy on the bucket used in the session policy verification and ran the request with a normal role to which no session policy was applied. The bucket policy is the only cause of denial.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyGetObject",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket-session/*"
    }
  ]
}
An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::123456789012:assumed-role/ExampleRole/ExampleSession is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::amzn-s3-demo-bucket-session/test.txt" with an explicit deny in a resource-based policy

The fact that the display changes depending on whether the policy is managed or inline is not described on the relevant pages of the S3 User Guide and the IAM User Guide that I checked at the time of writing; it is behavior I learned from this hands-on testing.

I tried two inline cases, session policies and identity-based policies, and in both the ARN was not displayed, so identifying the cause of denial remains a manual task as before. You still have to enumerate the inline policies set on the role and look for the matching statement by eye, so if ease of investigation is the priority, it is better to keep Deny statements in customer managed policies.

Distinguishing explicit Deny, implicit Deny, and pre-authentication failures

Even when a 403 is returned, the cause is not necessarily an explicit Deny.

I accessed a bucket owned by another account in the same Organizations using the AWS CLI. This is an implicit Deny, where there is simply no Allow. The HTTP status was 403, and while the reason for the denial was shown, no policy ARN was attached.

An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::123456789012:assumed-role/ExampleRole/ExampleSession is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::amzn-s3-demo-bucket-other-account/example-object.yaml" because no resource-based policy allows the s3:GetObject action

Next, I sent an HTTP request without credentials to an existing private bucket. The target was a bucket for which I had already confirmed HTTP 200 with an authenticated head-bucket. The HTTP status was 403, and the body was only a generic Access Denied.

curl -s https://amzn-s3-demo-bucket-private.s3.ap-northeast-1.amazonaws.com/
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>...</RequestId><HostId>...</HostId></Error>

When I accessed a nonexistent bucket name without authentication, HTTP 404 with NoSuchBucket was returned.

<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message><BucketName>amzn-s3-demo-bucket-no-such</BucketName>...</Error>

For one of the bucket names I accessed assuming it did not exist, HTTP 403 with AllAccessDisabled was returned. That bucket is likely to exist, with access disabled.

<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AllAccessDisabled</Code><Message>All access to this object has been disabled</Message>...</Error>

The criterion for judgment is the presence of a principal name. A message starting with User: arn:aws:sts:: has reached authorization evaluation, so it is worth tracing the policies. Among the four examples so far, only the implicit Deny falls into this category; the rest were a generic Access Denied, NoSuchBucket, and AllAccessDisabled. However, with a generic Access Denied, you cannot tell from the wording whether the cause is missing credentials or a policy denial.

In the next section, I confirm that the same generic message is returned when an unauthenticated request is denied by an RCP.

Even for public buckets, the ARN does not appear for unauthenticated requests

Among the five types for which the ARN is displayed, the one expected to apply to principals outside the organization is the RCP. An RCP controls access to resources within the organization, including by principals outside the organization. So I applied an RCP to a public bucket and checked whether the ARN is exposed to unauthenticated requests.

I applied an RCP that denies only s3:GetObject on denied.txt to a publicly accessible bucket. Before the RCP was applied, both denied.txt and allowed.txt could be read without authentication with HTTP 200.

After it was applied, an unauthenticated request to denied.txt returned 403.

HTTP/1.1 403 Forbidden
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>...</RequestId><HostId>...</HostId></Error>

When the same object was accessed from an authenticated principal in the same organization, the RCP's ARN was returned.

An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::123456789012:assumed-role/ExampleRole/ExampleSession is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::amzn-s3-demo-bucket-public/denied.txt" with an explicit deny in a resource control policy: arn:aws:organizations::111122223333:policy/o-exampleorgid/resource_control_policy/p-examplercpid2

As a control, allowed.txt in the same bucket could still be read without authentication with HTTP 200.

HTTP/1.1 200 OK
this object is publicly readable

The response body to the unauthenticated request did not include the policy ARN. I only measured unauthenticated requests; access from authenticated principals outside the organization was not verified. The S3 User Guide states that a generic Access Denied is returned for cross-account requests from outside the same organization.

Differences between CloudTrail and server access logs

When investigating after receiving a report, you may not have the full error message at hand. I checked which of CloudTrail and S3 server access logs lets you trace the policy ARN after the fact.

In CloudTrail S3 data events, the full error message is recorded in errorMessage. The policy ARN is retained as is.

{
  "eventVersion": "1.11",
  "eventName": "GetObject",
  "errorCode": "AccessDenied",
  "errorMessage": "User: arn:aws:sts::123456789012:assumed-role/ExampleRole/ExampleSession is not authorized to perform: s3:GetObject on resource: \"arn:aws:s3:::amzn-s3-demo-bucket-scp/test.txt\" with an explicit deny in a service control policy: arn:aws:organizations::111122223333:policy/o-exampleorgid/service_control_policy/p-examplescpid",
  "additionalEventData": {
    "httpStatusCode": 403
  }
}

However, object operations are not recorded as management events. To use it for after-the-fact investigation, you need to enable data events in advance. Note that recording data events incurs charges.

For S3 server access logs, I created a delivery configuration to CloudWatch Logs and checked the recordFields included in the response of create-delivery.

schema_version_id, bucket_arn, bucket_name, request_time, bucket_owner_id, remote_ip,
requester, request_id, operation, key_name, request_uri, http_status, error_code,
bytes_sent_size, object_size, total_duration, turn_around_duration, referer, user_agent,
version_id, host_id, signature_version, cipher_suite, authentication_type, host_header,
tls_version, access_point_arn, acl_required, source_region

There are http_status and error_code, but no field corresponding to the denial reason or the policy ARN. Therefore, the cause of a Deny cannot be identified from S3 server access logs alone. To trace down to the policy after the fact, CloudTrail data events are required.

Summary

S3's 403 Access Denied errors now include the ARN of the policy that denied access, making it easier to investigate the cause of an explicit Deny. If you manage Deny statements in policies that have an ARN, you can go directly from the error message to the relevant policy.

On the other hand, for policies that do not have an ARN, such as inline policies and bucket policies, you can still only learn the type as before. If ease of investigation is a priority, managing Deny statements as customer managed policies is effective.

Also, even when an unauthenticated request to a public bucket was denied by an RCP, the response was a generic Access Denied. There is no need to worry about the policy ARN being exposed to unauthenticated clients outside the organization.

This S3 change is part of an update that improves error messages for explicit Denies from SCPs, RCPs, and others. For details, see the AWS Security Blog article.

References


そのマルチアカウント運用、気合いで支えていませんか

Organizations や Control Tower で土台は作れても、アカウントもポリシーも増えるほど、運用は「詳しい一人」に寄りかかっていく。属人化が限界を迎える前に、組織として回す仕組み=CCoEへ。5,600社の支援から得た立ち上げの型を、無料資料にまとめました。

CCoE総合支援

組織で回す仕組みの資料をもらう

Share this article

AWSのお困り事はクラスメソッドへ