S3 AccessDenied Through a VPC Endpoint: Every Fix, in Order
If Amazon S3 works until the request goes through your VPC endpoint and then returns AccessDenied, check the endpoint policy first—but do not assume it is the only policy involved. The caller, VPC endpoint policy, S3 bucket policy, organization controls, and sometimes the AWS KMS key must all allow the request. The counterintuitive part is that setting the endpoint to Full Access does not grant the EC2 role S3 access; it only stops the endpoint policy itself from narrowing what can pass through that endpoint.
The fastest troubleshooting method is to stop asking “Does this server have S3 access?” and instead describe one exact request: this IAM principal is calling this S3 action against this ARN through this endpoint. That sentence gives you something concrete to compare against every policy.
A second clue is the failure type. A clean S3 HTTP 403 AccessDenied is an authorization clue. A timeout through an S3 interface endpoint points much earlier in the path toward DNS, security groups, network ACLs, or endpoint reachability. Do not troubleshoot those two symptoms as if they were the same problem.
What an S3 VPC endpoint AccessDenied really means
A VPC endpoint gives workloads a private route to a supported AWS service. For Amazon S3, you can use a gateway endpoint or an interface endpoint. Both can have an endpoint policy that controls which requests may use that endpoint.
The important phrase is endpoint policy. It is a permission control attached to the endpoint, not a replacement for IAM authorization.
That produces a common failure pattern:
- Your EC2 instance can resolve and reach S3.
- The request reaches the S3 service.
- S3 evaluates authorization.
- One applicable policy prevents the requested action.
- The client receives
403 AccessDenied.
In other words, network connectivity can be completely healthy while the API call is denied.
🙋♂️ Jake's Reality Check
"My EC2 role already has AmazonS3FullAccess. Why can the VPC endpoint still block me?"
The straight answer. Because the endpoint policy is another authorization gate. Broad permission on the role does not force a restrictive endpoint policy to allow the request.
Ethan explains it to Jake this way: “Think of the IAM role as your employee badge. The VPC endpoint is the door to the stockroom, and the bucket can have another lock behind it. One valid badge does not make every lock disappear.”
This is also why changing an EC2 security group usually does nothing for a clean endpoint-policy 403. Security groups control network traffic. An endpoint policy controls which API requests can use the endpoint. They solve problems at different layers.
The complete policy chain you should check
The simple version of this problem has three obvious policy layers: the caller's IAM permissions, the VPC endpoint policy, and the S3 bucket policy. Real AWS accounts can have more.
| Layer | Question | Typical failure |
|---|---|---|
| Identity policy | Can this user or role call the S3 action? | Missing s3:GetObject, s3:PutObject, or s3:ListBucket. |
| VPC endpoint policy | Can that request pass through this endpoint? | Wrong resource ARN, action, principal condition, or explicit Deny. |
| S3 bucket policy | Does the bucket accept the caller and request path? | Wrong aws:SourceVpce, aws:SourceVpc, or cross-account principal. |
| Permissions boundary | Is the role allowed to exercise its identity permissions? | The role policy says Allow, but the boundary does not allow the same action. |
| STS session policy | Were narrower permissions passed when the role was assumed? | Temporary credentials have less permission than the underlying role. |
| SCP / RCP | Does AWS Organizations permit the action? | Central organization policy restricts S3 or KMS. |
| KMS key policy | Is an SSE-KMS encrypted object involved? | S3 permission succeeds, but kms:Decrypt is unavailable. |
The rule that saves the most time is this: an applicable explicit Deny wins over an Allow. Adding another Allow is not a fix for an explicit Deny somewhere else.
There are also implicit denies. If the relevant policy context requires an Allow and no applicable Allow exists, the request remains denied even though you cannot find a statement literally saying "Effect": "Deny".
That is why a policy can look harmless while still limiting the role. A permissions boundary that simply omits s3:GetObject can reduce what the role may actually do even when the role's attached IAM policy contains an Allow.
Check the VPC endpoint policy before changing anything else
Open the VPC console, choose Endpoints, and select the S3 endpoint used by the workload. For a gateway endpoint, choose Actions → Manage policy.
If the endpoint is configured for Full Access, the endpoint policy is not intentionally narrowing requests. Do not spend the next hour adding S3 actions to an already broad endpoint policy. Move to the bucket, caller, organization, and encryption checks.
If it uses a custom policy, inspect four things separately:
- Principal. Which identity is meant to use the endpoint?
- Action. Is the exact S3 API permission present?
- Resource. Does the policy contain the correct bucket, object, or access-point ARN?
- Condition. Does every condition match the real request context?
A broad diagnostic policy can look like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
]
}
If a narrow custom endpoint policy produces the 403 and Full Access removes it while nothing else changes, you have isolated the endpoint policy as the important difference. Return to a narrow policy after diagnosis rather than leaving broad access simply because the test worked.
✅ Why this is the one to use
Change one authorization layer at a time. If you loosen the endpoint policy, bucket policy, role policy, and security group together, a successful retry tells you almost nothing about which change fixed the original failure.
The bucket ARN versus object ARN mistake
This mistake produces one of the strangest symptoms: listing the bucket succeeds, but downloading or uploading an object fails.
The bucket resource is:
arn:aws:s3:::shop-backups
The objects are:
arn:aws:s3:::shop-backups/*
s3:ListBucket applies to the bucket. s3:GetObject and s3:PutObject apply to objects.
| Task | Permission | Typical resource |
|---|---|---|
| List keys | s3:ListBucket | arn:aws:s3:::shop-backups |
| Download | s3:GetObject | arn:aws:s3:::shop-backups/* |
| Upload | s3:PutObject | arn:aws:s3:::shop-backups/* |
A policy that needs all three therefore commonly needs both resource forms:
{
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:ListBucket",
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::shop-backups",
"arn:aws:s3:::shop-backups/*"
]
}
Do not read “the bucket is allowed” as proof that objects beneath the bucket automatically match every S3 permission. Policy resources are evaluated against the action being requested.
Make sure you are troubleshooting the real caller
You can write a perfect endpoint policy for AppRole and still receive AccessDenied if the process is not actually using AppRole.
On the failing host, ask STS who is signing requests:
aws sts get-caller-identity
This is especially useful when credentials can come from multiple places:
- EC2 instance profile credentials.
- ECS or container task-role credentials.
- Environment variables such as
AWS_ACCESS_KEY_ID. - A named AWS CLI profile.
- An assumed role.
- Credentials injected by CI/CD tooling.
If your custom endpoint policy is built around a principal condition, compare that condition with the role behind the returned caller identity.
For gateway endpoint policies, a common pattern for restricting access to a role is to keep Principal as * and use a principal condition:
{
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:ListBucket",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::shop-backups",
"arn:aws:s3:::shop-backups/*"
],
"Condition": {
"ArnEquals": {
"aws:PrincipalArn": "arn:aws:iam::111122223333:role/AppRole"
}
}
}
If the application assumes another role before making S3 calls, or if somebody exported credentials from a different account into the shell, the condition can fail even though the EC2 instance itself has the expected instance profile.
The aws:SourceVpce bucket-policy trap
A bucket can be deliberately restricted so that requests must arrive through a particular VPC endpoint. This is a strong control, but it creates a very recognizable failure after an endpoint is replaced.
The bucket policy can contain a Deny like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnlessUsingApprovedEndpoint",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::shop-backups",
"arn:aws:s3:::shop-backups/*"
],
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-0abcdef1234567890"
}
}
}
]
}
Now imagine the original endpoint is deleted and a replacement is created. The new endpoint receives a new ID. Routing can point to the new endpoint perfectly, its endpoint policy can allow the operation, and the bucket can still reject the request because the bucket policy names the old ID.
⚠️ What this actually breaks
A bucket policy that denies requests unless they come through one VPC endpoint can also block normal AWS Management Console access to that bucket. Console requests do not originate from your named endpoint.
If several approved endpoints live in one VPC and your security design is based on the VPC rather than one endpoint, inspect whether aws:SourceVpc better represents the intended rule.
Do not casually remove a VPC restriction just to restore console access. First understand why it was added. The safest repair is usually correcting the value or exception that is wrong while preserving the security intent.
Why old IP restrictions can fail after the endpoint is added
Another sneaky case begins with a bucket policy that worked before the S3 gateway endpoint existed.
Perhaps S3 traffic originally left a private subnet through a NAT gateway, and the bucket policy was written around the public source IP seen on that path. You add a gateway endpoint, AWS starts routing S3 traffic through the endpoint, and the old IP-based condition stops representing the new request context.
For requests through a VPC endpoint, evaluate the VPC-aware condition keys intended for that context rather than assuming the old public source-IP condition still describes the request.
This is why the sentence “the only thing we changed was routing” does not rule out authorization. Some authorization policies use network-origin values as conditions.
🙋♂️ Jake's Reality Check
"So adding the endpoint can make an old bucket policy fail even when the endpoint policy itself is fine?"
Exactly. If the old policy was conditional on the previous network path, changing the path can change whether the condition matches.
The hidden second permission check: SSE-KMS
This is the edge case I would check immediately when ordinary S3 objects work but one group of objects still returns AccessDenied.
If an S3 object is encrypted with an AWS KMS key, reading the object involves authorization to use that key as well as authorization to read the S3 object.
For a download of an SSE-KMS encrypted object, the caller needs the relevant KMS decryption permission. That means a policy review limited to s3:GetObject can miss the real blocker entirely.
Suppose all of these are true:
- The endpoint allows
s3:GetObject. - The role allows
s3:GetObject. - The bucket policy does not deny the role.
- The route through the endpoint is correct.
You can still fail to read an object if the role is not permitted to use the KMS key protecting that object.
Check which encryption mode the failing object uses. If it uses SSE-KMS with a customer managed key, inspect both the identity's KMS permissions and the key policy that controls the key.
A narrowly scoped identity-policy statement can look conceptually like this:
{
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": "arn:aws:kms:us-east-1:111122223333:key/KEY-ID"
}
Do not paste that blindly. Replace the Region, account, and key ID with the key that actually protects the object, and make sure the key's own policy supports the intended principal and access model.
Uploads using KMS encryption can require additional KMS actions. Multipart upload behavior can also need KMS permissions beyond the S3 action you were initially troubleshooting.
This creates a very useful diagnostic split:
- If unencrypted or SSE-S3 objects succeed but SSE-KMS objects fail, inspect KMS.
- If everything in the bucket fails through the endpoint, start higher in the endpoint/bucket/IAM chain.
Jake's endpoint can be completely innocent here. The S3 request reaches the right bucket through the right private path, but the data is locked with a key his application is not allowed to use.
SCPs, RCPs, permission boundaries, and session policies
This section matters most in company AWS accounts where developers can see their application role but do not control every policy above it.
Service control policies
An AWS Organizations service control policy defines the maximum available permissions for principals in member accounts. Attaching AmazonS3FullAccess to a role does not let that role escape an applicable organization restriction.
If the organization hierarchy does not allow the required S3 action, the role's identity policy cannot enlarge that ceiling.
Resource control policies
Organizations can also place controls on resources. In accounts using these controls, do not assume the bucket policy is the only organization-level resource restriction involved.
Permissions boundaries
A permissions boundary limits the maximum permissions an IAM entity can exercise through the policy-evaluation rules that apply to that entity.
This produces the classic confusing screenshot: the role clearly shows s3:GetObject in an attached policy, but the request is denied. The missing part of the screenshot is the permissions boundary.
STS session policies
Temporary credentials can also be narrower than the base role. A caller that assumes a role with a restrictive session policy can lose access that somebody looking only at the role's attached policies expects it to have.
This is particularly relevant when an application works under one login method but fails through a deployment system or federated session that assumes the same named role differently.
✅ Why this is the one to use
When a role policy appears obviously correct, inspect the controls that cap that role before expanding the S3 policy again. Repeating the same Allow in three identity policies cannot overcome a restriction elsewhere in the evaluation chain.
For gateway endpoints, prove the subnet is actually using the endpoint
An endpoint policy only matters when the request uses that endpoint. For an S3 gateway endpoint, route-table association determines which subnets get the gateway-endpoint route.
- Open the VPC console.
- Select Endpoints.
- Select your S3 gateway endpoint.
- Open its route-table associations.
- Identify the route table associated with the EC2 subnet.
- Confirm that route table is associated with the endpoint.
- Inspect the route table for the S3 prefix-list route whose target is the endpoint.
- Confirm the workload is in the Region expected by the design.
A common mistake is checking a route table named “private” and assuming the instance uses it. The subnet association—not the route-table name—is what matters.
Gateway endpoints are designed for resources in the VPC using the endpoint. Their connectivity is not something you simply extend to another network through VPC peering, a transit gateway, VPN, or Direct Connect.
If the caller is actually on another network, the correct answer may be a different endpoint architecture rather than another route-table edit.
Also remember that creating or modifying gateway-endpoint routing can interrupt existing connections that were using the previous route. Applications should establish new connections over the new path.
For S3 interface endpoints, separate 403 from timeout
An interface endpoint is different from a gateway endpoint. It creates endpoint network interfaces in selected subnets and uses AWS PrivateLink.
If the client receives a clean S3 AccessDenied, authorization is still your main trail.
If the client instead times out, cannot establish TLS, or cannot resolve the intended hostname, check:
- The security group attached to the endpoint network interfaces.
- Inbound HTTPS on TCP 443 from the intended clients.
- Network ACL rules.
- The endpoint subnets.
- VPC DNS resolution.
- VPC DNS hostnames.
- Private DNS configuration.
- The exact S3 hostname used by the application.
You can explicitly test an interface endpoint using its endpoint-specific DNS name:
aws s3 ls s3://shop-backups \
--region us-east-1 \
--endpoint-url https://bucket.vpce-0a1b2c3d4e5f67890-abcd1234.s3.us-east-1.vpce.amazonaws.com
Copy the DNS name from your own endpoint details. Do not invent one from an endpoint ID and assume the resulting hostname is valid.
If the endpoint-specific test succeeds but the application's normal S3 hostname does not use the intended path, investigate DNS configuration rather than widening S3 permissions.
Cross-account S3 through the endpoint
Cross-account troubleshooting becomes much easier if you label every object by account.
Example:
- Account A owns the VPC and EC2 application.
- Account A owns the endpoint.
- Account B owns the S3 bucket.
- Account A's
BackupReaderrole needs to read objects in Account B.
The endpoint policy in Account A must allow the request through that endpoint. The caller in Account A needs applicable identity authorization. The resource side in Account B must permit the cross-account access. Organization and KMS controls can add more requirements.
A useful debugging sentence is:
Role arn:aws:iam::111122223333:role/BackupReader is calling s3:GetObject on arn:aws:s3:::central-backups/day1.json through vpce-0123456789abcdef0; the bucket belongs to account 444455556666.
Now compare that same request against each policy.
Do not expect enhanced S3 AccessDenied wording to solve every cross-account case. Some cross-account failures return only a generic Access Denied response, particularly outside the same AWS Organization.
If the bucket uses SSE-KMS with a customer managed key in Account B, add the KMS authorization model to this diagram as well. Fixing the bucket policy alone does not give the caller permission to use a key it is not authorized to use.
Worked example: one successful call, two different AccessDenied failures
Jake has three files in shop-backups:
public-config.jsonuses normal S3-managed encryption.orders.jsonuses the same bucket but is encrypted with a customer managed KMS key.archive/day1.jsonsits under the same bucket prefix.
His application role has:
s3:ListBucketonarn:aws:s3:::shop-backups.s3:GetObjectonarn:aws:s3:::shop-backups/*.
The endpoint policy mistakenly contains:
{
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:ListBucket",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::shop-backups"
}
Result one: listing works because ListBucket targets the bucket ARN.
Result two: reading public-config.json fails because the endpoint policy does not include the object ARN pattern.
Jake adds:
arn:aws:s3:::shop-backups/*
Now public-config.json works.
But orders.json still returns AccessDenied.
This second failure is not evidence that the endpoint fix failed. The two objects have different encryption requirements. The KMS-encrypted object adds the requirement that Jake's caller be authorized to use the KMS key.
He therefore has two different 403s that originally looked identical:
| Request | First failure | Fix |
|---|---|---|
| List bucket | None | Bucket ARN already matches. |
| Read normal object | Endpoint resource does not match object | Add bucket/* to the endpoint policy. |
| Read SSE-KMS object | KMS authorization still missing | Permit the required KMS use for the correct key. |
This is why good troubleshooting repeats the exact operation after every change instead of asking only “Does S3 work now?”
Console troubleshooting route from start to finish
- Identify the request. Write down the exact S3 operation, bucket, key, Region, and caller.
- Open VPC → Endpoints. Select the endpoint the workload should use.
- Confirm endpoint type. Gateway and interface endpoints have different network checks.
- Inspect the endpoint policy. Compare Principal, Action, Resource, and Condition with the real request.
- For a gateway endpoint, inspect route tables. Confirm the workload subnet uses a route table associated with the endpoint.
- For an interface endpoint, inspect network interfaces and security groups. If the symptom is timeout, check HTTPS reachability before S3 authorization.
- Open the S3 bucket permissions. Inspect the bucket policy for endpoint, VPC, principal, and explicit Deny conditions.
- Check encryption. Determine whether the failing object is SSE-KMS encrypted.
- Inspect the caller's IAM role. Check attached and inline identity policies.
- Inspect the permissions boundary. Do not assume the attached role policies define the complete maximum permission set.
- Check organization controls. If the account belongs to AWS Organizations, investigate applicable SCPs and other organization policies.
- Check how the session was created. A session policy can make temporary credentials narrower than the base role.
- Retry the same operation. Do not switch from GetObject to ListBucket and call that a successful test.
This sequence is deliberately repetitive about the exact operation. ListBucket, GetObject, and PutObject are not interchangeable tests.
CLI troubleshooting route
First identify the caller:
aws sts get-caller-identity
Then inspect the endpoint:
aws ec2 describe-vpc-endpoints \
--vpc-endpoint-ids vpce-0123456789abcdef0
Test the operation that is actually failing. For a bucket listing:
aws s3api list-objects-v2 \
--bucket shop-backups \
--region us-east-1
For a known object:
aws s3api get-object \
--bucket shop-backups \
--key public-config.json \
/tmp/public-config.json \
--region us-east-1
If you need to replace the endpoint policy, save the corrected JSON locally and run:
aws ec2 modify-vpc-endpoint \
--vpc-endpoint-id vpce-0123456789abcdef0 \
--policy-document file://policy.json
Do not use a successful bucket listing as proof that an object download permission is fixed. They are separate API permissions and can be denied for different reasons.
Do not forget S3 access points
Your endpoint policy can look perfect for a direct bucket request and still be wrong for an application that now uses an S3 access point.
An access point introduces its own ARN and policy context. A VPC-origin access point can also intentionally restrict where requests originate.
If the application configuration uses an access-point alias or access-point ARN, add these questions:
- Does the endpoint policy allow the access point involved?
- Does the access point policy allow the caller?
- Does the underlying bucket authorization support the request?
- Is the access point configured with the expected network origin?
- Is the application accidentally mixing a direct bucket URL with access-point policy assumptions?
This is an excellent example of why “I allowed the bucket ARN” may be an incomplete statement. Authorization follows the resource and request that the application really sends.
Use the symptom to choose the next check
| Symptom | Check first | Why |
|---|---|---|
| 403 began immediately after custom endpoint policy | Endpoint Action/Resource/Condition | The new policy is the strongest recent change. |
| 403 after endpoint replacement | aws:SourceVpce | The bucket may still contain the old endpoint ID. |
| List works, GetObject fails | Object ARN and s3:GetObject | Bucket and object permissions use different resource scopes. |
| Normal object works, KMS object fails | KMS authorization | The encrypted object adds a key-permission requirement. |
| Role policy clearly allows S3 but request still fails | Boundary, session policy, SCP/RCP | The visible role policy may not be the maximum effective permission. |
| Console cannot access bucket after endpoint restriction | Bucket explicit Deny | Console traffic does not originate through your endpoint. |
| Interface endpoint times out | DNS, SG, NACL, endpoint ENI | The failure occurs before normal S3 authorization feedback. |
| Works on laptop, fails on EC2 | Caller identity and VPC/bucket conditions | The credentials and request path may both differ. |
This table is also a good antidote to “permission roulette,” where every policy gets an extra wildcard until the application starts working and nobody knows why.
When nothing works: build one authorization packet
If you have been changing policies for an hour, stop editing and collect the request in one place.
Record:
- Exact timestamp of a failed request.
- Exact error text.
- Region.
- Caller account ID.
- Caller ARN from
sts get-caller-identity. - VPC ID.
- Subnet ID.
- Route table ID for gateway endpoints.
- Endpoint ID.
- Endpoint type.
- Endpoint policy.
- Bucket name.
- Bucket-owner account.
- Object key.
- Exact S3 API action.
- Bucket policy.
- Relevant identity policies.
- Permissions boundary if present.
- Role-assumption/session-policy details if applicable.
- SCP/RCP controls if applicable.
- Object encryption mode.
- KMS key ARN and access model if SSE-KMS is used.
- Access point ARN if one is involved.
Then reduce the issue to one sentence.
At 14:32 UTC, role X in account A called GetObject for object Y in bucket B through endpoint Z in us-east-1 and received AccessDenied.
That sentence is far more useful than “the private S3 endpoint is broken.”
If you need AWS Support, include that packet rather than only screenshots of the VPC console. It gives whoever investigates the issue the principal, action, resource, endpoint, Region, and policy context needed to trace the denial.
S3 VPC endpoint AccessDenied FAQs
Why does S3 return AccessDenied only through my VPC endpoint?
The endpoint adds another authorization policy to the request path, and your bucket policy might also contain conditions that behave differently for endpoint traffic. Compare the exact caller, S3 action, resource ARN, endpoint policy, and bucket conditions rather than assuming the role's IAM policy is the whole decision.
Does Full Access on the VPC endpoint give my EC2 role S3 access?
No. Full Access means the endpoint policy itself is not narrowing the request. The caller still needs applicable authorization, and the bucket, organization controls, permissions boundary, session policy, or encryption key can still prevent the operation.
Why does aws s3 ls work but GetObject returns AccessDenied?
Listing a bucket and reading an object are different permissions and use different resource scopes. Check s3:ListBucket against the bucket ARN and s3:GetObject against the object ARN pattern ending in /*.
Why does GetObject fail only for KMS-encrypted S3 objects?
SSE-KMS adds AWS KMS authorization to the request. A caller can have valid S3 GetObject permission and still fail because it cannot use the KMS key protecting the object. Check the relevant KMS permissions and key policy.
Can an S3 bucket policy override an explicit Deny in the VPC endpoint policy?
No. An applicable explicit Deny wins over an Allow elsewhere. Fix the policy that contains the effective Deny rather than adding broader Allows to unrelated policies.
Why did S3 fail after I replaced the VPC endpoint?
Check the S3 bucket policy for an aws:SourceVpce condition that still contains the old endpoint ID. A replacement endpoint has a different ID, so a Deny based on the old value can reject requests through the new endpoint.
Why does a VPC-only bucket policy block the AWS console?
Console requests do not originate through your specified VPC endpoint. A bucket policy that explicitly denies requests unless they use that endpoint can therefore deny console access as part of the rule's intended behavior.
How do I know which IAM role is actually making the S3 request?
Run aws sts get-caller-identity using the same credentials and environment as the failing application or shell. Do not assume the instance profile is the caller if environment credentials, a profile, task role, or assumed role might be used.
Can a permissions boundary cause S3 AccessDenied even when the role policy allows S3?
Yes. A permissions boundary participates in the effective permission calculation for the IAM entity. The attached identity policy does not by itself prove the action is within the caller's effective permissions.
Can an STS session policy cause the same VPC endpoint AccessDenied error?
It can contribute to the effective denial. Temporary credentials created with a restrictive session policy can have less permission than the base role appears to have, so inspect how the failing session was created.
Can an AWS Organizations SCP block S3 through a VPC endpoint?
Yes. Organization controls define limits that member-account principals cannot exceed through ordinary role permissions. If IAM and endpoint policies look correct, check whether the account sits under organization policies that restrict the requested S3 or KMS action.
How do I know whether my EC2 subnet is actually using the S3 gateway endpoint?
Identify the route table associated with the EC2 subnet and verify that it contains the S3 prefix-list route targeting the gateway endpoint. Do not rely on the route table's name; verify the subnet association.
Why does my S3 interface endpoint time out instead of returning AccessDenied?
A timeout usually points to connectivity earlier in the request path. Check DNS, endpoint network interfaces, endpoint security groups, TCP 443, network ACLs, and the hostname being used before focusing on S3 policy semantics.
Can a resource in a peered VPC use my S3 gateway endpoint?
Do not design around extending gateway-endpoint connectivity outside the VPC through peering, transit gateways, VPN, or Direct Connect. Choose an endpoint and network architecture supported for the caller's real location.
Does an S3 access point need to appear in my endpoint authorization design?
Yes when the application uses that access point. Inspect the access point policy, network origin, endpoint policy resources, and underlying bucket authorization instead of assuming a policy written only for direct bucket ARNs covers the access-point request.
What should I collect before opening an AWS Support case for this 403?
Collect the exact error and timestamp, caller ARN, account IDs, Region, VPC and subnet IDs, route table if relevant, endpoint ID and type, endpoint policy, bucket and object, S3 action, bucket policy, identity policies, boundary and session-policy details, organization controls, encryption mode, KMS key information, and access-point details if used.
The shortest decision path that still catches the hard cases
If you want the whole article reduced to one troubleshooting order, use this:
- Run
aws sts get-caller-identity. Know the caller. - Write down the exact S3 operation and exact target ARN.
- Identify the endpoint ID and endpoint type.
- Read the endpoint policy literally: Principal, Action, Resource, Condition.
- If it is a gateway endpoint, prove the workload subnet uses an associated route table.
- If it is an interface endpoint and the symptom is timeout, fix connectivity first.
- Read the S3 bucket policy, especially explicit Deny statements and VPC/endpoint conditions.
- Check whether the object is SSE-KMS encrypted.
- Inspect the caller's identity policies.
- Inspect its permissions boundary.
- Inspect STS session restrictions if temporary credentials are involved.
- Inspect AWS Organizations controls.
- For cross-account access, check both account sides.
- For access points, include the access-point policy and ARN.
- Retry the same API operation after each targeted change.
This order avoids two expensive debugging habits: widening permissions before you know which policy failed, and testing a different API operation from the one your application actually needs.
Jake does not need five new wildcard policies just because his overnight backup suddenly receives a 403. He needs the identity, action, resource, endpoint, and encryption context written down in one place. Once those are explicit, the mystery usually shrinks into one mismatched ARN, stale endpoint ID, missing action, restricted session, organization policy, or KMS key permission.
If this error is holding up a deployment or a backup job, keep the investigation boring and repeatable: one request, one change, the same request again. That is much safer than opening every permission door until the 403 disappears.
📌 If you keep one line from this page
An S3 VPC endpoint policy is one permission gate in a chain, not a replacement for IAM, the bucket policy, organization controls, or KMS.
When the endpoint says yes, another applicable control can still say no.
Revision note. Written October 3, 2026. A 403 through an endpoint feels like a wall, but it is one more policy in the path. Name the caller, action, resource and key, and the denier usually steps forward.