S3 VPC Gateway Endpoints let your instances in a private subnet talk to S3 without traversing the public internet.

Here’s a quick demo. We’ll create a private subnet, launch an EC2 instance, and then show it accessing S3.

First, the VPC setup:

{
  "VpcId": "vpc-0123456789abcdef0",
  "CidrBlock": "10.0.0.0/16",
  "IsDefault": false,
  "State": "available",
  "Tags": [
    {
      "Key": "Name",
      "Value": "my-s3-vpc"
    }
  ]
}

And a private subnet within it:

{
  "VpcId": "vpc-0123456789abcdef0",
  "CidrBlock": "10.0.1.0/24",
  "State": "available",
  "MapPublicIpOnLaunch": false,
  "Tags": [
    {
      "Key": "Name",
      "Value": "my-private-subnet"
    }
  ]
}

Now, the crucial part: the S3 Gateway Endpoint. You create this in your VPC. When you create it, you specify the VPC and the service, which is com.amazonaws.REGION.s3. AWS automatically assigns it an endpoint ID.

{
  "VpcId": "vpc-0123456789abcdef0",
  "VpcEndpointId": "vpce-0abcdef1234567890",
  "ServiceName": "com.amazonaws.us-east-1.s3",
  "State": "available",
  "CidrBlocks": [],
  "DnsEntries": [
    {
      "DnsName": "vpce-0abcdef1234567890.s3.us-east-1.vpce.amazonaws.com",
      "IpAddress": "10.0.1.100"
    }
  ],
  "PolicyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Principal": "*",
        "Effect": "Allow",
        "Action": "*",
        "Resource": "*"
      }
    ]
  },
  "RouteTableIds": [
    "rtb-0fedcba9876543210"
  ],
  "CreationTimestamp": "2023-10-27T10:00:00Z"
}

Notice the RouteTableIds. When you create the endpoint, AWS automatically updates the route tables associated with the subnets you select (or all subnets in the VPC if you don’t specify) to include a route for the S3 service. This route points to the endpoint, directing S3 traffic through it. The specific IP address 10.0.1.100 in DnsEntries is an internal IP within your VPC that the endpoint uses.

Let’s launch an EC2 instance in my-private-subnet (10.0.1.0/24). It won’t have a public IP. We’ll need an IAM role attached to it that grants S3 read access.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-private-bucket",
        "arn:aws:s3:::my-private-bucket/*"
      ]
    }
  ]
}

Now, SSH into the instance (you’d typically use a bastion host or Systems Manager Session Manager for this). Once inside, try to access an S3 bucket. Without the endpoint, this would fail because there’s no internet gateway or NAT device. With the endpoint configured, it works seamlessly.

$ aws s3 ls s3://my-private-bucket
2023-10-27 10:15:00 my-objects/

The magic is in the route table. The rtb-0fedcba9876543210 associated with my-private-subnet will have an entry like this:

{
  "DestinationCidrBlock": "vpce-0abcdef1234567890.s3.us-east-1.amazonaws.com/32",
  "GatewayId": "vpce-0abcdef1234567890",
  "State": "active",
  "Origin": "CreateRoute",
  "RouteTableId": "rtb-0fedcba9876543210"
}

This route directs all traffic destined for the S3 service’s public IP range (which AWS internally maps to the endpoint’s IP) through the VPC endpoint. The endpoint then forwards the traffic to the S3 service.

The key advantage is that traffic stays within the AWS network, never touching the public internet. This improves security, reduces latency, and avoids NAT gateway costs for S3 traffic.

A common misconception is that the endpoint’s IP address (like 10.0.1.100 in the example) is what your EC2 instance directly routes to. Instead, the route table entry uses the service’s DNS name, and the endpoint acts as a network interface within your VPC that resolves and handles this traffic.

When you configure the endpoint policy, it acts as a firewall for S3 access from within your VPC. You can restrict which buckets can be accessed and from which principals (like specific IAM roles on your EC2 instances).

The next thing you’ll likely encounter is needing to access other AWS services privately, which leads to Interface Endpoints.

Want structured learning?

Take the full S3 course →