You've checked the IAM policy. The bucket policy looks fine. The instance profile is attached. But your EC2 instance in a private subnet still gets AccessDenied — or worse, just times out — when trying to reach S3.

In most cases, the problem is not IAM. It's the network path.

What's Actually Happening

Without a VPC Gateway Endpoint for S3, traffic from a private subnet reaches S3 by going through:

EC2 (private subnet) → NAT Gateway → Internet Gateway → S3 (public endpoint)

This path has two problems:

$0.045/GB
NAT Gateway data processing charge applied to every byte of S3 traffic without a VPC endpoint

The Fix: Free VPC Gateway Endpoint

A VPC Gateway Endpoint for S3 routes traffic directly over the AWS internal network, completely bypassing NAT Gateway. It's free — no hourly charge, no per-GB fee.

# Get the route table ID for your private subnets
aws ec2 describe-route-tables \
  --filters Name=vpc-id,Values=vpc-0abc123 \
  --query 'RouteTables[*].[RouteTableId,Tags[?Key==`Name`].Value|[0]]'

# Create the S3 Gateway Endpoint
aws ec2 create-vpc-endpoint \
  --vpc-id vpc-0abc123 \
  --service-name com.amazonaws.us-east-1.s3 \
  --route-table-ids rtb-private123

# Verify it was created
aws ec2 describe-vpc-endpoints \
  --filters Name=service-name,Values=com.amazonaws.us-east-1.s3 \
  --query 'VpcEndpoints[*].[VpcEndpointId,State,VpcId]'

After creating the endpoint, AWS automatically adds a route to the specified route table pointing S3-bound traffic (pl-xxxxxx prefix list) to the endpoint. The change takes effect immediately — no restart needed.

Side effect: Every GB of S3 traffic that previously went through NAT Gateway now saves $0.045. For workloads moving significant data to/from S3, this frequently cuts NAT Gateway bills by 40–70%.

If It's Still Failing After Adding the Endpoint

Check these in order:

1. Endpoint policy is too restrictive. The default endpoint policy allows all S3 access. If someone modified it, check:

aws ec2 describe-vpc-endpoints \
  --vpc-endpoint-ids vpce-0abc123 \
  --query 'VpcEndpoints[*].PolicyDocument'

2. Bucket policy blocks access. Some bucket policies use aws:sourceVpc or aws:sourceVpce conditions. Ensure your VPC ID or endpoint ID is listed:

aws s3api get-bucket-policy --bucket my-bucket | jq '.Policy | fromjson'

3. Wrong route table associated. The endpoint must be associated with the route table of the subnet your instance is in. Check that the prefix list route appears in the right table:

aws ec2 describe-route-tables \
  --route-table-ids rtb-private123 \
  --query 'RouteTables[*].Routes[?DestinationPrefixListId!=null]'

4. Cross-region bucket. The endpoint only covers the region it was created in. If the bucket is in us-west-2 but the endpoint is in us-east-1, traffic still goes via NAT. You need an endpoint in each region.

Also Worth Checking: DynamoDB

The same pattern applies to DynamoDB. If you have Lambda functions or EC2 instances in private subnets writing to DynamoDB, add a Gateway Endpoint for DynamoDB too — same command, different service name:

aws ec2 create-vpc-endpoint \
  --vpc-id vpc-0abc123 \
  --service-name com.amazonaws.us-east-1.dynamodb \
  --route-table-ids rtb-private123

Related Articles

→ AWS NAT Gateway Cost: Why Your Bill Is Too High [4 Fixes] → VPC Flow Logs: Read Fields and Query with Athena → AWS Internet Gateway Cost: What You Actually Pay

Frequently Asked Questions

Why is S3 access denied from a private subnet even though IAM looks correct?

Without a VPC Gateway Endpoint, S3 traffic exits via NAT Gateway to the public internet. If a bucket policy has a aws:sourceVpce condition, or if NAT is missing, requests fail despite correct IAM permissions.

Does a VPC Gateway Endpoint for S3 cost anything?

No. VPC Gateway Endpoints for S3 and DynamoDB are completely free. They route traffic over the AWS internal network, eliminating NAT Gateway data processing charges of $0.045/GB.

Do I need to restart my EC2 instance after adding a VPC endpoint?

No. The endpoint takes effect immediately — AWS adds the route to your route table and existing connections can use the new path without any instance restart.