VPC Endpoints: Interface, Gateway, and Keeping Traffic Off the Public Internet

2 min readCloud Infrastructure

VPC endpoints let resources in private subnets reach AWS services without internet access. Gateway endpoints (S3 and DynamoDB) are free and route-table-based. Interface endpoints (everything else) deploy ENIs in your subnets and cost money. Choosing the wrong endpoint type or skipping private DNS causes unexpected public internet routing.

awsvpcprivatelink

Three endpoint types

| Type | How it works | Services | Cost | |---|---|---|---| | Gateway | Route table entry → S3/DynamoDB prefix list | S3, DynamoDB only | Free | | Interface | ENI deployed in subnet, DNS resolves to private IP | Most AWS services | $0.01/hour/AZ + $0.01/GB | | Gateway Load Balancer | ENI for transparent traffic inspection | Third-party appliances | Variable |

Gateway endpoints are the correct choice for S3 and DynamoDB — they're free and don't require DNS changes. Interface endpoints (powered by PrivateLink) work for everything else.

Private DNS must be enabled for Interface endpoints to intercept SDK calls

GotchaAWS VPC Endpoints

Interface endpoints create ENIs in your subnets with private IP addresses. Without private DNS enabled, the service's public DNS name (e.g., secretsmanager.us-east-1.amazonaws.com) still resolves to public IPs — traffic bypasses the endpoint and goes through the internet or NAT. Private DNS makes the public hostname resolve to the endpoint ENI's private IP inside the VPC.

Prerequisites

  • Route 53 Resolver
  • ENI basics
  • AWS PrivateLink

Key Points

  • private_dns_enabled = true rewrites the public service hostname to resolve to endpoint ENI IPs within the VPC.
  • Requires enableDnsHostnames and enableDnsSupport to be enabled on the VPC.
  • Without private DNS, the AWS SDK uses public hostnames and routes to public IPs — the interface endpoint is bypassed.
  • For Gateway endpoints, private DNS is irrelevant — routing is handled by prefix list entries in route tables.

Gateway endpoints: S3 and DynamoDB

Gateway endpoints add a prefix list entry to route tables. Traffic to S3 or DynamoDB goes through the endpoint rather than the internet or NAT.

# S3 Gateway endpoint — free, no ENI
resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.us-east-1.s3"
  vpc_endpoint_type = "Gateway"

  # Associate with route tables to route S3 traffic through endpoint
  route_table_ids = [
    aws_route_table.private.id,
    aws_route_table.public.id,
  ]
}

After creating the endpoint, AWS adds a managed prefix list entry (e.g., pl-63a5400a) to the associated route tables. Any traffic to S3 IP ranges matches this prefix and routes through the endpoint.

Check that the endpoint is associated with the correct route tables:

aws ec2 describe-vpc-endpoints \
  --filters "Name=service-name,Values=com.amazonaws.us-east-1.s3" \
  --query "VpcEndpoints[].{ID:VpcEndpointId,State:State,RouteTables:RouteTableIds}"

Interface endpoints: all other services

# Secrets Manager interface endpoint
resource "aws_vpc_endpoint" "secretsmanager" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.us-east-1.secretsmanager"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids   # ENI deployed in these subnets
  security_group_ids  = [aws_security_group.vpc_endpoint.id]
  private_dns_enabled = true   # critical: makes SDK use private endpoint
}

resource "aws_security_group" "vpc_endpoint" {
  name   = "vpc-endpoint-sg"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [aws_vpc.main.cidr_block]
  }
}

The security group on the endpoint controls which VPC resources can reach it — the endpoint's ENI must allow inbound 443 from resources that need to use it.

For high availability, deploy endpoints in multiple AZs:

resource "aws_vpc_endpoint" "ecr_api" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.us-east-1.ecr.api"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids   # one subnet per AZ
  security_group_ids  = [aws_security_group.vpc_endpoint.id]
  private_dns_enabled = true
}

# ECR also requires the docker endpoint and S3 gateway endpoint
resource "aws_vpc_endpoint" "ecr_dkr" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.us-east-1.ecr.dkr"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoint.id]
  private_dns_enabled = true
}

ECR requires two endpoints: ecr.api for the ECR API and ecr.dkr for Docker image pulls. Both are needed, plus an S3 gateway endpoint (ECR stores image layers in S3).

📝Common endpoint combinations for private ECS/EKS clusters

A private ECS/EKS cluster with no internet access needs endpoints for every AWS service it calls. Missing any one causes timeouts during task startup or image pulls:

locals {
  interface_endpoints = [
    "com.amazonaws.${var.region}.ecr.api",
    "com.amazonaws.${var.region}.ecr.dkr",
    "com.amazonaws.${var.region}.ecs",
    "com.amazonaws.${var.region}.ecs-agent",
    "com.amazonaws.${var.region}.ecs-telemetry",
    "com.amazonaws.${var.region}.logs",           # CloudWatch Logs
    "com.amazonaws.${var.region}.ssm",            # SSM Session Manager
    "com.amazonaws.${var.region}.ssmmessages",
    "com.amazonaws.${var.region}.ec2messages",
    "com.amazonaws.${var.region}.secretsmanager",
  ]
}

resource "aws_vpc_endpoint" "services" {
  for_each = toset(local.interface_endpoints)

  vpc_id              = aws_vpc.main.id
  service_name        = each.value
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoint.id]
  private_dns_enabled = true
}

# S3 gateway endpoint for ECR image layer pulls
resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.${var.region}.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = [aws_route_table.private.id]
}

Each interface endpoint costs ~$7.20/month/AZ ($0.01/hour × 3 AZs × 24 × 30). A full private ECS setup with 10 interface endpoints across 3 AZs costs ~$216/month in endpoint charges before data transfer.

An ECS task in a private subnet uses the AWS SDK to call Secrets Manager. You created an Interface VPC endpoint for Secrets Manager with private_dns_enabled=false. The task's security group allows outbound HTTPS. The call times out. Why?

medium

The Secrets Manager endpoint is deployed and active. The endpoint's security group allows inbound 443. There is no NAT Gateway in this VPC — the private subnet has no internet access.

  • AThe ECS task's security group needs to explicitly allow traffic to the endpoint's ENI IP addresses
    Incorrect.The task's security group allows all outbound HTTPS. This isn't the issue — the endpoint's security group (allowing inbound from the VPC) handles the other side.
  • BWithout private_dns_enabled=true, the SDK resolves secretsmanager.us-east-1.amazonaws.com to public IPs. With no internet access, the connection times out — the endpoint ENIs are never used
    Correct!The Interface endpoint exists and is reachable by private IP, but the SDK doesn't know to use it. Without private DNS, secretsmanager.us-east-1.amazonaws.com resolves to public IPs (52.x.x.x). The task tries to connect to those public IPs, but with no internet access, the connection times out. The fix is private_dns_enabled=true, which makes the public hostname resolve to the endpoint ENI's private IP inside the VPC. SDK code requires no changes.
  • CInterface endpoints don't support Secrets Manager — only Gateway endpoints do
    Incorrect.Gateway endpoints only support S3 and DynamoDB. Secrets Manager uses an Interface endpoint (PrivateLink). The endpoint type is correct.
  • DThe endpoint must be in the same subnet as the ECS task
    Incorrect.Interface endpoints create ENIs in specified subnets and are reachable from any subnet in the VPC that can route to the endpoint ENI. The endpoint doesn't need to be in the same subnet as the task.

Hint:The endpoint is deployed. Does the SDK know to route traffic to it instead of the public internet?