S3 Cross-Account Access Denied: Both Policies, Then the Deny
If Amazon S3 returns AccessDenied when an IAM user or role in one AWS account tries to use a bucket in another account, check both sides of the permission bridge. For normal direct cross-account access, the caller needs an identity-based IAM policy that allows the required S3 action, and the bucket needs a resource-based bucket policy that allows that external principal. The surprising part is that a bucket policy can look completely correct and the request can still fail: AWS evaluates the trusted account and the trusting account separately, and both evaluations must return Allow.
The easiest mental model is two locked doors. Account B controls whether its own employee is allowed to perform the job. Account A controls whether that employee is allowed through the bucket's door. Opening only one door does not open the other.
That sounds simple, but S3 permission failures become confusing because several other controls can sit around those two basic policies. The fastest route is therefore not “add more S3 permissions.” It is to identify the caller, action, resource, and account boundary first.
Why S3 cross-account access normally needs two policies
Imagine two AWS accounts:
- Account A:
111122223333. It owns the S3 bucket. - Account B:
444455556666. It owns the IAM role that wants to read the bucket.
Suppose the bucket is:
arn:aws:s3:::jake-shop-backups
And Account B uses:
arn:aws:iam::444455556666:role/BackupReader
When BackupReader directly requests an object from the bucket, AWS has to answer two separate questions.
The first is inside Account B: is this IAM role allowed to perform the requested S3 action against the external resource?
The second is inside Account A: does the resource owner permit that external principal to perform the requested operation?
For this normal direct cross-account pattern, both sides must approve the request.
| Policy | Account | What it decides |
|---|---|---|
| Identity-based IAM policy | Account B | May this user or role request the S3 operation? |
| S3 bucket policy | Account A | May this external principal use this bucket or its objects? |
| Limiting controls | Either side / organization | Does another rule deny or limit what those Allows can do? |
🙋♂️ Jake's Reality Check
"I put Account B in the bucket policy already. Doesn't that mean Account B can use the bucket?"
No. You authorized the request from the bucket owner's side. The specific identity in Account B still needs its own permission to make that request.
Ethan explains it this way: “You gave the visitor permission to walk through your door. You still haven't proved his own badge lets him perform the job. Cross-account access checks both.”
This is why adding a perfectly formed bucket policy can appear to do nothing. The policy may be correct; it may simply be only half of the authorization path.
First prove which identity is really calling S3
Before changing either policy, run:
aws sts get-caller-identity
This is one of the most useful commands in AWS permission troubleshooting because it answers the question developers often skip: who am I right now?
A laptop can use one CLI profile while the application uses another. EC2 can use an instance profile. ECS can use a task role. Lambda can use its execution role. An AWS IAM Identity Center session can resolve to a role. A CI job can assume a deployment role. An application can call STS and change identities again.
If you are editing a bucket policy for BackupReader while the real caller is ProductionAppRole, no amount of polishing the BackupReader statement will solve the failure.
With a named AWS CLI profile:
aws sts get-caller-identity --profile account-b
Write down the returned account ID and ARN. Do not settle for “it is somewhere in Account B.” The exact role or user matters.
If the failure happens from EC2, run the identity check from the EC2 environment. If it happens in a container, run it using the container's credentials. A successful request from your laptop proves the laptop's credentials work. It does not prove the application's credentials work.
Find the exact S3 action that is failing
“S3 access” is too broad to troubleshoot.
A workload might need to list a bucket, read an object, upload an object, inspect tags, retrieve a version, or check a bucket setting. Each operation maps to its own permission requirements.
Start with the exact operation that fails.
For example:
- Listing object keys commonly needs
s3:ListBucket. - Reading an object commonly needs
s3:GetObject. - Uploading an object commonly needs
s3:PutObject.
Do not add s3:* just because you do not yet know which operation the application performs. Find the operation first.
A useful diagnostic is to reduce the application to one direct CLI request.
To test listing:
aws s3api list-objects-v2 \
--bucket jake-shop-backups
To test a known object:
aws s3api get-object \
--bucket jake-shop-backups \
--key daily/orders.csv \
orders.csv
If the list fails but the known object downloads, your problem is not simply “the role cannot access S3.” The role may have object permission but lack bucket-list permission.
If listing works while object download fails, investigate GetObject, the object resource ARN, encryption, object ownership, and applicable conditions.
Bucket ARN vs object ARN: the small difference that causes big failures
One of the most common S3 policy mistakes is using the correct action with the wrong resource ARN.
The bucket ARN is:
arn:aws:s3:::jake-shop-backups
An object resource pattern is:
arn:aws:s3:::jake-shop-backups/*
Those are not interchangeable.
| Task | Permission | Resource shape |
|---|---|---|
| List object keys | s3:ListBucket | arn:aws:s3:::BUCKET |
| Download objects | s3:GetObject | arn:aws:s3:::BUCKET/* |
| Upload objects | s3:PutObject | arn:aws:s3:::BUCKET/* |
Consider this policy:
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::jake-shop-backups"
}
The intent is obvious: allow downloads from that bucket. But the resource points at the bucket itself rather than its objects.
For objects beneath the bucket, the resource pattern needs the object portion:
"Resource": "arn:aws:s3:::jake-shop-backups/*"
That single /* is why “I already allowed GetObject” is not enough information. Action and resource have to match.
Account B: configure the caller's IAM policy
Now configure the trusted account: the account containing the identity that makes the request.
For a role named BackupReader that must list the bucket and download objects, a narrow identity policy can separate the bucket operation from the object operation:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListExternalBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::jake-shop-backups"
},
{
"Sid": "ReadExternalObjects",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::jake-shop-backups/*"
}
]
}
This policy answers only Account B's side of the question.
It does not force Account A to accept the role.
That distinction is worth repeating because it explains the most common failure pattern in this entire problem: “The role has S3 permission, but the other account's bucket still denies it.”
Of course it does, if the bucket owner has not granted the cross-account access.
If your workload only reads known keys and never lists the bucket, do not blindly add s3:ListBucket because a tutorial included it. Grant what the workload actually needs.
If the workload must upload rather than download, use the appropriate object action and limit it to the required bucket, prefix, or object set wherever possible.
✅ Why this is the one to use
Keep the caller's policy narrow enough that a future engineer can tell exactly which external bucket it was designed to use. Broad s3:* permission hides mistakes instead of explaining them.
Account A: configure the bucket policy
Next configure the trusting account: the account that owns the S3 resource.
Open:
Amazon S3 → Buckets → jake-shop-backups → Permissions → Bucket policy.
A direct read-only example for the external role can look like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAccountBRoleToList",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:role/BackupReader"
},
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::jake-shop-backups"
},
{
"Sid": "AllowAccountBRoleToRead",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:role/BackupReader"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::jake-shop-backups/*"
}
]
}
Here, Principal identifies who outside the bucket owner's account receives the permission.
Action identifies what that principal may do.
Resource identifies which bucket or objects the permission covers.
The Account B identity policy and Account A bucket policy now describe the same relationship from opposite sides.
Do not replace the role ARN with "Principal":"*" merely to see whether the error disappears. That converts a targeted cross-account test into a much broader resource-policy change and can create a different security problem.
⚠️ What this actually breaks
Making the principal or resource unnecessarily broad can grant access to identities or objects that were never part of your original requirement. Fix the missing permission instead of turning the policy into a universal test switch.
Test the complete cross-account path in the right order
Once both basic policies exist, test in a controlled order.
- Confirm the caller.
aws sts get-caller-identity - Test the smallest operation you actually need.
aws s3api get-object \ --bucket jake-shop-backups \ --key daily/orders.csv \ orders.csv - If listing is required, test it independently.
aws s3api list-objects-v2 \ --bucket jake-shop-backups - Read the entire failure message. Do not reduce a detailed error to the two words “Access Denied.”
- Only then inspect the next policy layer. If both basic Allows are correct, move to explicit denies, KMS, organization controls, boundaries, session policies, endpoint policies, conditions, ownership, and Requester Pays.
This test order prevents a common debugging mistake: changing five things at once and then not knowing which change mattered.
If GetObject succeeds but your application still fails, the application is probably doing more than GetObject. Capture the next failed API operation instead of widening the successful one.
Worked example: 1,250 objects and one misleading Access Denied
Jake's backup bucket contains 1,250 objects.
His Account B application has s3:GetObject permission for:
arn:aws:s3:::jake-shop-backups/*
The Account A bucket policy also allows the Account B role to call s3:GetObject.
Downloading a known object therefore has the right two-sided S3 permission path.
But Jake opens a script that first runs ListObjectsV2 so it can discover every key. A ListObjectsV2 response returns up to 1,000 keys per request, so listing 1,250 keys is naturally a paginated job: the first response can return up to 1,000 and another request is needed for the remaining keys.
Jake's script never gets that far.
The listing call itself returns AccessDenied.
Why?
Because both policies contain s3:GetObject, but neither grants the separate s3:ListBucket bucket-level action.
The download permission is fine. The discovery operation is not.
That distinction matters because Jake could spend an hour changing object ARNs even though the failing request targets the bucket.
The correct Account B addition is:
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::jake-shop-backups"
}
And Account A needs the corresponding bucket-policy permission for that cross-account principal.
The useful lesson is not “always grant ListBucket.” It is this: grant the operation the application actually performs, against the resource type that operation uses, on both required sides of the cross-account boundary.
🙋♂️ Jake's Reality Check
"But I can download a file if I know its name. How can the bucket still say I don't have access?"
Because “access to the bucket” is not one permission. Reading a known object and listing the bucket are separate operations.
The four combinations of the two basic policies
Strip away the extra controls for a moment and the direct cross-account model becomes a simple matrix.
| Account B identity allows | Account A bucket allows | Basic cross-account result |
|---|---|---|
| No | No | Denied |
| Yes | No | Denied |
| No | Yes | Denied |
| Yes | Yes | Can proceed if no other applicable control denies or limits it |
The last row is deliberately not labeled “always allowed.”
Two basic Allows are necessary for the direct cross-account pattern, but they are not magical overrides. Another policy layer can still deny or limit the request.
That is why the troubleshooting process has two phases.
Phase one asks, “Did we create the basic two-sided permission?”
Phase two asks, “What else participates in the final authorization decision?”
An explicit Deny beats the Allow you just added
AWS permissions begin from denial. Policies grant specific permission by Allow statements, but an applicable explicit Deny overrides an Allow.
That creates a frustrating-looking configuration:
The role policy says Allow.
The bucket policy says Allow.
S3 still returns AccessDenied.
That does not prove either Allow is malformed.
There may be a matching Deny elsewhere.
For example, imagine a policy contains:
{
"Effect": "Deny",
"Action": "s3:*",
"Resource": "*"
}
If that Deny applies to the request, another Allow does not cancel it.
Do not treat IAM like arithmetic where two Allows outweigh one Deny. A relevant explicit Deny wins.
Also inspect Deny statements containing conditions. The statement can look harmless until the request satisfies its condition.
⚠️ What this actually breaks
Do not delete a company-wide Deny just to make one test pass. First identify why it matches your request. That Deny may be enforcing TLS, approved networks, approved organization membership, encryption, or another security requirement.
What changed in S3 Access Denied messages in 2026
🕐 What changed between versions
- Earlier enhanced S3 denial messages could tell you the policy type and reason for a denial in supported scenarios.
- On August 13, 2026, S3 added the specific IAM or AWS Organizations policy ARN for supported explicit-deny cases in same-account and same-organization requests.
- The covered policy types include SCPs, RCPs, identity-based policies, session policies, and permission boundaries.
This is useful because a message that identifies the responsible policy ARN can send you directly to the control that denied the request.
But do not overread the feature.
Your topic here is cross-account access, and cross-account can mean accounts inside the same organization or accounts in different organizations. The enhanced context is not a promise that every possible cross-account 403 will identify the exact offending policy.
So read the full error first.
If it names a specific policy ARN, investigate that policy before changing the bucket.
If it does not, continue with the full authorization checklist.
Ethan's rule is simple: “A detailed 403 is evidence. Use the evidence you have before inventing a new permission problem.”
Both policies are present but S3 still returns Access Denied
Once the Account B IAM policy and Account A bucket policy are both correct, work down this list in order.
- Run
aws sts get-caller-identityagain from the failing environment. Do not assume the application is using the credentials you tested on your laptop. - Identify the exact failing API operation. A successful
GetObjectdoes not proveListBucketpermission. - Check the resource ARN. Separate bucket-level and object-level permissions.
- Search for an explicit Deny. Inspect the identity policy and bucket policy, then continue to organization and session controls.
- Check AWS Organizations policies. An SCP or RCP can affect the authorization path.
- Check the caller's permissions boundary. A boundary can limit permissions available to the identity.
- Check temporary-session restrictions. A session policy can make an assumed-role session narrower than the role's identity policy appears to be.
- Check KMS. SSE-KMS with a customer managed key creates another permission path.
- Check the S3 VPC endpoint policy. This is especially important when a request works from a laptop but fails from a VPC workload.
- Check bucket-policy conditions. Source VPC, VPC endpoint, TLS, organization, account, ARN, and other conditions can change whether a statement applies.
- Check Requester Pays. If enabled, the requester side must handle the Requester Pays requirement correctly.
- Check object ownership and ACL history. This is particularly useful when only a subset of older objects fails.
Notice what is not in that list: “make everything * and hope.”
A broad Allow may hide the specific error during a test, but it gives you less information about what the application actually requires.
SCPs, RCPs, permission boundaries, and session policies
Many S3 tutorials stop after the two obvious policies. Enterprise AWS environments often do not.
An AWS Organizations service control policy, or SCP, can restrict what principals in organization member accounts can do.
A resource control policy, or RCP, can restrict access involving covered resources in organization accounts.
A permissions boundary sets a maximum permission boundary for an IAM user or role's identity-based permissions.
A session policy can narrow temporary credentials created for an assumed-role or federated session.
The practical lesson is this: seeing an Allow attached to a role does not necessarily mean the role session has unrestricted access to everything mentioned in that Allow.
Jake discovers this the hard way after a company administrator introduces a stricter organization policy on Friday. His role policy is unchanged. His bucket policy is unchanged. Saturday morning, the backup job fails.
“So the bucket broke itself overnight?” Jake asks.
Ethan answers: “No. You are looking only at the two documents you own. Authorization can include controls somebody else owns.”
This is also where the newer explicit-denial details can help. If the returned error identifies a policy ARN, hand that ARN to the administrator responsible for it. “This exact policy denied this exact call” is a much stronger support request than “S3 is broken.”
KMS-encrypted objects add another permission bridge
S3 permission and KMS permission are separate.
If the object is protected with SSE-KMS using a customer managed KMS key, a cross-account reader can have perfectly correct S3 permission and still fail because it cannot use the KMS key.
For the cross-account customer-managed-key case, inspect these layers:
- Account A's S3 bucket policy.
- Account B's IAM permission for the S3 operation.
- The customer managed KMS key policy in Account A.
- Account B's IAM permission for the necessary KMS operation.
The KMS key also belongs to a Region, and the relevant customer managed key for the S3 scenario must be correctly configured for that bucket and access path.
One particularly important restriction: cross-account permission cannot be solved by editing the key policy of an AWS managed KMS key, because you do not control that key policy. The cross-account customer-managed-key design is the one you can configure.
That gives you a valuable symptom test.
If unencrypted or differently encrypted objects work while objects encrypted under one customer managed KMS key fail, stop widening the bucket policy and inspect the key path.
✅ Why this is the one to use
Debug S3 and KMS as separate authorization layers. A missing KMS permission cannot be repaired by granting broader S3 access.
Why the same credentials can work locally and fail inside a VPC
This symptom is so useful that it deserves its own section:
Your laptop succeeds. The application running in AWS fails.
First, compare identities. They may not be the same credentials at all.
If they really are using equivalent intended permissions, compare network paths.
An application inside a VPC can access S3 through an S3 VPC endpoint. That endpoint has a policy, and its policy can restrict which principals, S3 resources, or operations are usable through that path.
This means the same logical S3 request can face an additional network-resource policy when made through the endpoint.
Do not immediately conclude that S3 behaves differently on EC2.
Run the caller-identity test in the failing environment, confirm whether an S3 endpoint is used, then inspect that endpoint policy.
Also look for bucket-policy conditions tied to a particular VPC or VPC endpoint. A request made from your laptop will not carry the same VPC context as a request made through the expected endpoint.
Jake once tests a role from his office laptop and announces, “The role works.” Ethan asks one annoying question: “From the same path as the app?” It is annoying because it is usually the right question.
A correct Principal can still fail because of Conditions
A bucket-policy statement is more than Principal, Action, and Resource.
A Condition can decide whether the statement applies to the current request.
Conditions can be used to require or restrict request properties such as network source, VPC endpoint, organization membership, principal characteristics, transport security, or other context.
This explains another “but the ARN is right” failure.
Suppose the statement correctly names:
arn:aws:iam::444455556666:role/BackupReader
But the statement also requires a request to arrive through a particular VPC endpoint.
The correct role calling from the wrong route will not satisfy that permission condition.
Likewise, a Deny statement can contain a condition. The relevant question becomes:
Does this entire statement match this request?
Not:
Does my role ARN appear somewhere in this JSON?
When a policy contains several conditions, inspect them one by one against the actual failing request context.
Object Ownership, ACL history, and Requester Pays
New S3 buckets use the Bucket owner enforced Object Ownership setting by default. In that model, ACLs are disabled and the bucket owner owns the objects.
That modern default removes much of the old ACL-based ownership complexity.
But older buckets and objects can still create a symptom that confuses people:
Most objects work, but a few specific objects return Access Denied.
When that happens, compare those objects instead of rewriting the policy for the entire bucket.
Ask:
- Were the failing objects uploaded by a different account?
- Are they older than the objects that work?
- Does their encryption differ?
- What Object Ownership mode does the bucket currently use?
- Is ACL-based behavior still relevant to those objects?
Another separate branch is Requester Pays.
If Requester Pays is enabled for the bucket, requests that require acknowledgment of requester charges must be made accordingly. A caller can otherwise run into an access error even after you have spent time checking IAM policies.
That is why the troubleshooting checklist includes settings that do not look like “permissions” at first glance.
When only one bucket fails and Requester Pays is enabled on that bucket, investigate the setting before editing organization-wide IAM policies.
Alternative: assume a role in the bucket owner's account
Direct cross-account bucket access is not the only design.
Account A can create a role in Account A. Account B receives permission to assume it. The Account A role receives the S3 permissions needed for Account A's bucket.
The flow becomes:
- Account A creates a role.
- The role's trust policy allows the intended Account B principal to assume it.
- Account B's identity policy allows
sts:AssumeRolefor that Account A role. - The Account A role's permission policy allows the required S3 operations.
- Account B assumes the role and uses the returned temporary credentials.
After the caller assumes the Account A role, S3 sees permissions through an identity that belongs to Account A rather than through the original direct cross-account bucket-access pattern.
That can be useful when Account B needs several Account A resources and you want one delegated role to represent the whole job.
It does not mean every cross-account S3 setup should be redesigned around STS.
Ethan's preference is situational: “One external role reading one shared bucket can be perfectly understandable with direct bucket-policy access. If the workload needs a bundle of resources in Account A, I start thinking about an Account A role because it gives that bundle one permission home.”
The important thing is not to mix the two models mentally.
If your caller is assuming into Account A, debug the assumed role and trust relationship.
If your Account B role accesses the Account A bucket directly, debug the two-account resource-policy path described throughout this article.
Access Points and Access Grants are different authorization paths
Before copying a bucket-policy fix into every environment, check how the application actually addresses S3.
It may be using an S3 access point rather than the bucket name directly.
An access point has its own access-point policy, and the underlying bucket also participates in the authorization model. Cross-account access through an access point therefore needs the policies for that architecture, not merely a direct bucket example copied from somewhere else.
S3 Access Grants is another access-management model for granting access to S3 data. Cross-account Access Grants uses its own instance resource policies, grants, and credential flow.
So identify which of these you actually have:
- Direct bucket access with an external IAM identity.
- An S3 access point.
- S3 Access Grants.
- An IAM role assumed into the bucket owner's account.
All four can involve cross-account data access, but their policy paths are not identical.
This article's “both policies needed” rule is specifically about the ordinary direct cross-account resource-policy pattern.
Symptom-first S3 cross-account diagnostic table
| Symptom | First place to look | Why |
|---|---|---|
| Bucket policy allows Account B but everything fails | Account B IAM policy | The caller's account still has to authorize the request. |
| Account B role has S3 Allow but external bucket fails | Account A bucket policy | The resource owner has not necessarily trusted the caller. |
| GetObject works but listing fails | s3:ListBucket on bucket ARN | Object read and bucket list are separate actions. |
| Listing works but download fails | s3:GetObject, object ARN, KMS | Bucket list permission does not grant object read. |
| Laptop succeeds but EC2 fails | Caller identity and VPC endpoint | The identity or network-policy path may differ. |
| Only KMS-encrypted objects fail | KMS key policy and IAM KMS permission | KMS has its own authorization layer. |
| Only older objects fail | Ownership, ACL history, encryption | Object-specific history can differ. |
| Error identifies a policy ARN | That exact policy | The denial message has already narrowed the search. |
| Everything looks allowed but request is denied | Explicit denies and limiting policies | An Allow does not override a relevant Deny. |
When nothing works, stop adding permissions and collect evidence
When you have edited IAM for an hour, the temptation is to try:
"Action": "*",
"Resource": "*"
Do not use that as your troubleshooting strategy.
Instead collect a small evidence packet:
- The complete Access Denied message.
- The output of
aws sts get-caller-identityfrom the failing environment. - The exact API operation.
- The exact bucket and object key, if an object request fails.
- The caller's relevant IAM policy statements.
- The bucket's relevant policy statements.
- Any permissions boundary on the caller.
- Any session policy applied to the credentials.
- Relevant SCP or RCP controls.
- Whether the request traverses an S3 VPC endpoint.
- Whether the object uses a customer managed KMS key.
- Whether Requester Pays is enabled.
- Whether every object fails or only a subset.
- The bucket's Object Ownership mode.
Then reproduce the failure with one narrow request.
If GetObject is the problem, test one known object.
If listing is the problem, test list-objects-v2 directly.
If uploading is the problem, test one controlled object rather than the whole application workflow.
This gives an AWS administrator or Support engineer four concrete things to reason about:
principal + action + resource + policy context.
That is far more useful than screenshots showing that two policies contain the word “Allow.”
Frequently asked questions about S3 cross-account Access Denied
Does S3 cross-account access need both an IAM policy and a bucket policy?
For the normal direct cross-account pattern where an IAM principal in Account B accesses an S3 bucket in Account A, yes. The caller's account needs an identity-based policy allowing the requested access, and the bucket owner's account needs a resource-based policy allowing the external principal. Both account evaluations must allow the request.
Why does my S3 bucket policy allow Account B but I still get Access Denied?
The specific IAM user or role in Account B might not have an identity-based policy that permits the required S3 action. Confirm the caller with aws sts get-caller-identity, then inspect that identity's permission policy.
Can an IAM policy alone grant access to a bucket in another AWS account?
Not in the normal direct resource-policy cross-account model. The identity's own account must permit the operation, and the resource-owning account must authorize the external access as well.
Can a bucket policy alone give a cross-account role access?
A bucket policy can grant the resource owner's side of the permission, but the external role still needs appropriate identity-based permission in its own account for normal direct cross-account access.
Why can I GetObject but not ListBucket cross-account?
They are different S3 actions against different resource types. s3:GetObject applies to object resources such as arn:aws:s3:::bucket/*. s3:ListBucket applies to the bucket resource arn:aws:s3:::bucket.
Why can I list the bucket but not download an object?
Listing proves only that the list operation is permitted. Check s3:GetObject, the object ARN, bucket-policy object permission, any matching Deny, object encryption, KMS access, and object-specific ownership differences.
How do I see which IAM role is actually getting the S3 Access Denied error?
Run aws sts get-caller-identity using the same credentials and environment that fail. Compare the returned account and ARN with the principal you intended to authorize.
Does an explicit Deny override an Allow in my S3 policies?
Yes. A relevant explicit Deny overrides applicable Allow permissions. Adding another Allow does not cancel the Deny.
Can an SCP cause S3 cross-account Access Denied?
Yes. An AWS Organizations service control policy can limit what principals in member accounts are permitted to do. Inspect organization controls when the account is governed by AWS Organizations.
Can a permissions boundary block S3 even when the IAM role policy says Allow?
Yes. A permissions boundary can limit permissions available through identity-based policies. Include the boundary when you trace the role's effective permission path.
Why does cross-account S3 work on my laptop but fail from EC2?
First verify that both environments use the same intended identity. Then inspect differences in the request path, especially an S3 VPC endpoint policy and bucket-policy conditions tied to a VPC or endpoint.
Why do KMS-encrypted S3 objects fail from another account?
S3 authorization and KMS authorization are separate. With a customer managed KMS key, cross-account access can require the external identity's IAM permissions plus appropriate permission in the KMS key policy as well as the S3 permission path.
Why do only some objects in the bucket return Access Denied?
Compare the failing objects with objects that work. Check encryption, KMS key use, upload history, object ownership, ACL history on older configurations, and whether the policy targets the exact prefix or object set.
Do new S3 buckets need ACLs for cross-account access?
New buckets use Bucket owner enforced Object Ownership by default, which disables ACLs. IAM policies and bucket policies are the normal direct cross-account mechanism rather than relying on ACLs.
Should I use a bucket policy or assume a role in the bucket owner's account?
Both are valid architectures. Direct bucket-policy access lets the external identity access the S3 resource when both sides authorize it. An assume-role design creates a role in the bucket owner's account and lets the external principal obtain temporary credentials for that role. Choose the model that matches how you want to manage the wider set of Account A resources.
What should I check first when cross-account S3 access suddenly stops working?
Read the complete Access Denied message, run aws sts get-caller-identity from the failing environment, identify the exact API operation, and confirm its resource ARN. If the two basic policies are unchanged, investigate new explicit denies, SCPs, RCPs, permission boundaries, session policies, VPC endpoint policies, KMS permissions, and bucket-policy conditions.
The checklist to keep beside your next 403
Cross-account S3 authorization feels complicated when you look at every AWS policy type at once. It becomes much simpler when you walk the request from the caller toward the resource.
- Run
aws sts get-caller-identity. - Write down the exact Account B role or user.
- Name the failing S3 API operation.
- Determine whether that operation targets the bucket, an object, or another S3 resource type.
- Confirm Account B's identity policy permits the operation.
- Confirm Account A's bucket policy permits that external principal and operation.
- Check for explicit Deny statements.
- Read any enhanced denial context and policy ARN in the returned error.
- Check SCPs, RCPs, permission boundaries, and session policies.
- Check KMS when a customer managed key protects the failing object.
- Check the S3 VPC endpoint policy if the request uses an endpoint.
- Evaluate every relevant bucket-policy condition against the actual request.
- Check Requester Pays.
- Investigate Object Ownership and ACL history when only certain objects fail.
- If the application still fails, reduce it to one direct API request and collect the full error.
Jake's original mistake was understandable. He thought the bucket policy was the permission because the bucket was the thing he wanted to protect. The missing piece was realizing that cross-account authorization is a conversation between two accounts, not a command from one policy.
Ethan leaves him with the rule worth remembering: “Start with the identity, action, and resource. Then make sure both accounts agree. Only after that should you hunt for the thing that is saying no.”
If your Account A bucket already trusts Account B and the 403 still refuses to move, resist the urge to make the policy wider. Confirm the actual caller and its IAM permission first, then follow the denial path one layer at a time. That keeps the fix understandable after the emergency is over.
📌 If you keep one line from this page
For direct S3 cross-account access, the caller's account must allow the request and the bucket owner's account must allow the external principal.
If those two Allows are correct, look for the layer that is still denying or limiting the request.
Revision note. Written October 1, 2026, with the August policy-ARN detail in 403 messages included. Two accounts, two policies, one quiet denier: you'll find it sooner than it feels right now.