Right, let’s talk about getting to S3 without the internet. Because frankly, the public internet is a bit of a mess. It’s loud, unpredictable, and frankly, a bit of a security risk when you’re trying to have a private conversation between your pristine VPC and an AWS service. You don’t want your sensitive data taking a scenic route through a dozen routers; you want a private, direct line. That’s what AWS PrivateLink and Interface Endpoints are for.

Think of an Interface Endpoint as a fancy, internal extension phone. Instead of your application in a private subnet dialing out to a public S3 URL (s3.amazonaws.com), which requires a NAT Gateway and all that jazz, it just picks up the internal phone. The call never leaves the Amazon network. It’s a direct, private connection from your VPC to the service. The magic here is that it provides this connectivity not through a gateway, but by plopping a network interface (an Elastic Network Interface, or ENI) directly into your chosen subnet. This ENI gets a private IP address from your subnet’s range, and that becomes the entry point for all your traffic to that service.

Why You’d Bother With This

The “why” is simpler than the “how,” for once. You do this for two rock-solid reasons: security and cost.

  • Security: This is the big one. Your traffic to, say, S3, never crosses the public internet. It stays entirely within the AWS network. This closes a huge attack vector. It also allows you to keep your instances in private subnets with no outbound internet path at all, making them truly locked down, while still letting them access essential services.
  • Cost: Remember that NAT Gateway you needed to give your private instances outbound internet access? The one that costs ~$32 a month plus for the privilege of existing, plus ~$0.045 per GB for data processing? Yeah, you can often ditch that for service traffic. Data processed through an Interface Endpoint is free. You read that right. Free. The endpoint itself has a small hourly cost and a per-GB data processing charge, but it often ends up being cheaper than a NAT Gateway for high-throughput service traffic.

The DNS Sleight of Hand

This is where the real voodoo happens, and AWS’s implementation is genuinely clever. When you create an endpoint, it gives you a new DNS name to use. For an S3 endpoint in us-east-1, it’ll look like bucket.s3.us-east-1.amazonaws.com. The brilliant part? This name resolves to the private IPs of the endpoint ENIs in your VPC.

If you’re in us-east-1 and you query the normal public S3 endpoint from within the VPC, you’ll get a public IP. But if you query the PrivateLink endpoint hostname, it returns a private IP address from your subnet. Your application doesn’t need to be completely rewritten; it often just needs to be pointed to the correct regional endpoint URL, which is usually a config change.

Creating an Endpoint: The Nitty-Gritty

Let’s make one. You’ll do this for services like S3, DynamoDB, SSM, you name it. The CLI command is straightforward, but the policy is where most people mess up.

# Let's create an endpoint for S3 in us-east-1
aws ec2 create-vpc-endpoint \
    --vpc-id vpc-12345678 \
    --service-name com.amazonaws.us-east-1.s3 \
    --vpc-endpoint-type Interface \
    --subnet-ids subnet-abc12345 subnet-def67890 \
    --security-group-id sg-0a1b2c3d4e5f67890 \
    --private-dns-enabled

Now, about that policy. It controls which resources can be accessed through the endpoint. The default policy is a wide-open “Allow *” which is… not great. Let’s not do that. Let’s be specific.

{
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": "arn:aws:s3:::my-secure-bucket/*"
    },
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": "arn:aws:s3:::my-secure-bucket/classified/*"
    }
  ]
}

You’d save this as a JSON file and reference it with --policy-document file://my-policy.json in the CLI command. This policy says “only allow access to objects in my-secure-bucket, and explicitly deny access to anything in the classified/ prefix.” This is a classic allow/deny pattern for fine-grained control.

The Gotchas and “Oh, Crap” Moments

This isn’t all rainbows and free data transfer. Here’s what they don’t always highlight in the marketing copy:

  1. DNS is Your Boss: If your application is hardcoded to use a public endpoint or a generic endpoint, it will bypass your Interface Endpoint. You must configure your app to use the standard AWS regional endpoint (e.g., s3.us-east-1.amazonaws.com). The PrivateLink DNS magic only works if you use that format. Using the global s3.amazonaws.com endpoint? That’ll still go public.
  2. Security Groups Are Bouncers: The ENI for the endpoint lives in your subnet and must obey Security Group rules. You must configure the Security Group you attach to the endpoint to allow inbound traffic on port 443 (for HTTPS) from the source you want (e.g., your application instances’ Security Group). This is the number one reason for “my endpoint is created but nothing works.”
  3. The Subnet Problem: You have to specify subnets for the ENI to live in. If you only put it in one AZ, and your application instance is in another, the traffic will have to cross AZ boundaries (incurring cost and latency). Always deploy endpoints in every AZ where you have resources that need to use them.
  4. It’s Not for Everything: PrivateLink is for AWS services. If you need to connect to your own application or a third-party SaaS, you’re looking for a Network Load Balancer and Gateway Endpoints, which are a different, slightly less magical beast for S3 and DynamoDB only.

So, is it worth it? For production workloads that need to talk to AWS services, it’s almost a non-brainer. The security posture is infinitely better, and the cost savings on NAT Gateways can be significant. Just mind your DNS and your Security Groups, and you’ll have that private line installed in no time.