Guide · AWS VPC · Private Connectivity

AWS VPC Endpoints for MCP Servers — Interface vs Gateway, Private DNS, and Service Endpoint Setup

When an MCP server running in a VPC calls AWS services like Secrets Manager, S3, or Bedrock without a VPC endpoint, every API call exits the VPC through a NAT Gateway, crosses the public internet, and re-enters the AWS network — costing $0.045/GB in NAT fees and adding unnecessary exposure. VPC endpoints route those calls directly within the AWS backbone, eliminating NAT costs and ensuring traffic never traverses the public internet. There are two types: Gateway endpoints (free, only for S3 and DynamoDB, work by adding a route table entry) and Interface endpoints (all other services including Secrets Manager, Lambda, Bedrock, ECR, SQS, and CloudWatch Logs; $0.01/hour per AZ per endpoint; provision an ENI in your subnet). The most important Interface endpoint option is private DNS — when enabled, the standard AWS service hostname resolves to the endpoint's private IP inside the VPC, so your MCP server code requires zero changes to use the endpoint.

TL;DR

Add Gateway endpoints for S3 and DynamoDB (free — just a route table update). For Secrets Manager, ECR, Bedrock, SQS, and CloudWatch Logs, create Interface endpoints with --private-dns-enabled in each AZ's subnet. Attach a security group to each Interface endpoint that allows inbound TCP 443 only from your MCP server's security group. With private DNS enabled, the existing secretsmanager.us-east-1.amazonaws.com hostname automatically resolves to the endpoint ENI's private IP — no code change needed. Your MCP server then requires no NAT Gateway or internet gateway to call AWS services.

Gateway endpoints — free, S3 and DynamoDB only

Gateway endpoints work by injecting a prefix list route into your subnet's route table. Traffic destined for the S3 or DynamoDB IP ranges is routed to the endpoint rather than the default internet gateway or NAT gateway. There is no ENI, no security group, no hourly charge — just a route table entry and an optional endpoint policy.

# Create a Gateway endpoint for S3
aws ec2 create-vpc-endpoint \
  --vpc-endpoint-type Gateway \
  --service-name com.amazonaws.us-east-1.s3 \
  --vpc-id vpc-0abc123 \
  --route-table-ids rtb-0private1 rtb-0private2

# Create a Gateway endpoint for DynamoDB
aws ec2 create-vpc-endpoint \
  --vpc-endpoint-type Gateway \
  --service-name com.amazonaws.us-east-1.dynamodb \
  --vpc-id vpc-0abc123 \
  --route-table-ids rtb-0private1 rtb-0private2

# Verify routes were added
aws ec2 describe-route-tables \
  --route-table-ids rtb-0private1 \
  --query 'RouteTables[0].Routes[?GatewayId!=`null`]'
# Expect entries with GatewayId starting with "vpce-" for S3 and DynamoDB prefixes

Attach only the route tables for subnets where your MCP servers run. Public subnets rarely need gateway endpoints — their default route already reaches S3 via the internet gateway, and adding a gateway endpoint there only affects routing (the S3 traffic still goes to the same regional S3 endpoint, just via the AWS backbone instead of the internet). For MCP servers in private subnets with a NAT gateway, adding S3 and DynamoDB Gateway endpoints eliminates all NAT processing fees for those two services, which are often the highest-volume AWS API calls.

Interface endpoints — all services, private DNS configuration

Interface endpoints provision an Elastic Network Interface (ENI) in each subnet you specify. The ENI gets a private IP address in the subnet's CIDR range. With private DNS enabled, AWS overrides the public DNS resolution for the service within your VPC — requests to secretsmanager.us-east-1.amazonaws.com resolve to the endpoint ENI's private IP instead of the public Secrets Manager endpoint. This means no code change is required in your MCP server.

# Create Interface endpoint for AWS Secrets Manager with private DNS
aws ec2 create-vpc-endpoint \
  --vpc-endpoint-type Interface \
  --service-name com.amazonaws.us-east-1.secretsmanager \
  --vpc-id vpc-0abc123 \
  --subnet-ids subnet-0private-a subnet-0private-b \
  --security-group-ids sg-0mcp-endpoint \
  --private-dns-enabled

# Create Interface endpoint for Amazon ECR API
aws ec2 create-vpc-endpoint \
  --vpc-endpoint-type Interface \
  --service-name com.amazonaws.us-east-1.ecr.api \
  --vpc-id vpc-0abc123 \
  --subnet-ids subnet-0private-a subnet-0private-b \
  --security-group-ids sg-0mcp-endpoint \
  --private-dns-enabled

# ECR Docker registry also needs a separate DKR endpoint
aws ec2 create-vpc-endpoint \
  --vpc-endpoint-type Interface \
  --service-name com.amazonaws.us-east-1.ecr.dkr \
  --vpc-id vpc-0abc123 \
  --subnet-ids subnet-0private-a subnet-0private-b \
  --security-group-ids sg-0mcp-endpoint \
  --private-dns-enabled

# Create Interface endpoint for Amazon Bedrock runtime
aws ec2 create-vpc-endpoint \
  --vpc-endpoint-type Interface \
  --service-name com.amazonaws.us-east-1.bedrock-runtime \
  --vpc-id vpc-0abc123 \
  --subnet-ids subnet-0private-a subnet-0private-b \
  --security-group-ids sg-0mcp-endpoint \
  --private-dns-enabled

# Query endpoint ENI IP addresses
aws ec2 describe-vpc-endpoints \
  --filters Name=service-name,Values=com.amazonaws.us-east-1.secretsmanager \
  --query 'VpcEndpoints[0].NetworkInterfaceIds'

ECR requires two endpoints: ecr.api for control-plane calls (DescribeImages, GetAuthorizationToken) and ecr.dkr for the Docker registry protocol (image layer pushes and pulls). If you're running ECS or ECS Fargate tasks that pull images at container start time, you also need an S3 Gateway endpoint — ECR stores image layers in S3, and the layer pulls go directly to S3 after the manifest is fetched through the ecr.dkr endpoint.

Security group for Interface endpoints

The security group attached to an Interface endpoint controls which resources in the VPC can reach the endpoint. This is your primary mechanism for scoping AWS service access: only the MCP server's security group can call Secrets Manager through the endpoint, not every EC2 instance in the VPC. Configure the endpoint security group with inbound TCP 443 from the MCP server's security group ID.

# Create a security group for VPC endpoints
ENDPOINT_SG=$(aws ec2 create-security-group \
  --group-name MCPVPCEndpoints \
  --description "Controls access to VPC endpoints for AWS services" \
  --vpc-id vpc-0abc123 \
  --query 'GroupId' --output text)

# Allow HTTPS (443) only from MCP server security group
aws ec2 authorize-security-group-ingress \
  --group-id $ENDPOINT_SG \
  --protocol tcp \
  --port 443 \
  --source-group sg-0mcpserver

# Deny all other inbound traffic (default deny is implicit — no rule needed)

# To also allow ECS task execution role to pull images:
aws ec2 authorize-security-group-ingress \
  --group-id $ENDPOINT_SG \
  --protocol tcp \
  --port 443 \
  --source-group sg-0ecs-tasks

# Verify the security group
aws ec2 describe-security-groups \
  --group-ids $ENDPOINT_SG \
  --query 'SecurityGroups[0].IpPermissions'

Do not leave the endpoint security group open to 0.0.0.0/0 on port 443. Any EC2 instance in the VPC with a route to the endpoint ENI could then call Secrets Manager — bypassing the intent of scoping endpoint access. Using security group references (source group) instead of CIDR blocks is important: it works even if the MCP server's IP address changes due to Auto Scaling or container replacement, and it prevents other instances in the same subnet CIDR from reaching the endpoint.

Common MCP server endpoint stack

A typical production MCP server in a private VPC subnet needs endpoints for at least six services. This table shows the endpoint type, service name suffix, and whether multiple endpoints are needed per service.

AWS ServiceEndpoint typeService name suffixNotes
S3Gateway (free)s3Covers Lambda deployment packages, ECR layer pulls, CloudTrail delivery
DynamoDBGateway (free)dynamodbRequired if MCP server uses DynamoDB for tool state or session storage
Secrets ManagerInterfacesecretsmanagerEliminates NAT for credential fetches on every container cold start
ECR API + DKRInterface × 2ecr.api, ecr.dkrBoth required for container image pulls; S3 Gateway needed for layers
Amazon Bedrock runtimeInterfacebedrock-runtimeRequired if MCP server calls Claude or other Bedrock models
SQSInterfacesqsRequired if tools enqueue work to SQS queues
CloudWatch LogsInterfacelogsRequired for Lambda and ECS containers to write logs without NAT
STSInterfacestsRequired for IAM role assumption — called on every credential refresh

STS is easy to overlook — every AWS SDK call involving role assumption, temporary credentials, or cross-account access calls STS first. If your MCP server uses an IAM execution role (which it should), STS is called at credential refresh time (every 15 minutes for ECS task roles). Without an STS endpoint, that refresh call goes through NAT. The STS endpoint also enables you to scope which principals can assume roles from within your VPC using endpoint conditions in IAM policy.

Verifying endpoint connectivity

After creating endpoints, verify they are working from within the VPC before deploying your MCP server. The most common failure is a missing endpoint or misconfigured security group preventing the DNS override from resolving to a reachable IP.

# From an EC2 instance in the VPC (or via SSM Session Manager — no bastion needed)
# Verify Secrets Manager endpoint DNS resolves to a private IP
nslookup secretsmanager.us-east-1.amazonaws.com
# With private DNS enabled: returns 10.x.x.x (VPC private IP)
# Without endpoint or private DNS disabled: returns 52.x.x.x (public IP)

# Test HTTPS connectivity to the endpoint
curl -v https://secretsmanager.us-east-1.amazonaws.com \
  --connect-timeout 5 2>&1 | grep -E "Connected|SSL|error"
# Expect: "Connected to secretsmanager.us-east-1.amazonaws.com (10.x.x.x)"

# Test actual API call (requires valid IAM credentials on the instance)
aws secretsmanager list-secrets --region us-east-1
# Should succeed even if the NAT gateway is removed

# Check which endpoint ENIs exist in a subnet
aws ec2 describe-network-interfaces \
  --filters "Name=subnet-id,Values=subnet-0private-a" \
            "Name=interface-type,Values=vpc_endpoint" \
  --query 'NetworkInterfaces[*].{ID:NetworkInterfaceId,IP:PrivateIpAddress,Desc:Description}'

If nslookup returns a public IP despite the endpoint existing, check that enableDnsHostnames and enableDnsSupport are both enabled on the VPC — Interface endpoint private DNS requires both. Run aws ec2 describe-vpc-attribute --vpc-id vpc-xxx --attribute enableDnsSupport to verify. If the DNS is correct but the API call times out, the endpoint security group is blocking the connection — ensure the inbound rule references the instance's security group, not just an IP range.

Failure modes reference

FailureSymptomFix
Private DNS not resolving to private IPnslookup returns public IP; traffic still goes through NATVPC must have both enableDnsHostnames and enableDnsSupport enabled; enable them with aws ec2 modify-vpc-attribute
Interface endpoint connection timeoutSDK call hangs then fails with connection timeout, not auth errorEndpoint security group missing inbound 443 rule from instance's security group; verify with describe-security-groups and the endpoint's NetworkInterfaceIds
ECR image pull fails in FargateECS task fails to start with "CannotPullContainerError"Need both ecr.api and ecr.dkr Interface endpoints plus S3 Gateway endpoint; ecr.dkr alone is insufficient — the API endpoint handles GetAuthorizationToken and GetDownloadUrlForLayer
Lambda in VPC cannot call S3S3 PutObject times out; Lambda has no NAT gateway routeAdd Gateway endpoint for S3 to the Lambda function's subnet route table; Gateway endpoints are free and the only option for S3 in private subnets
Endpoint exists but specific principal cannot access serviceAccessDeniedException despite correct IAM roleEndpoint policy is restricting access — check the endpoint policy with describe-vpc-endpoints; default policy allows all, but a customized policy may block specific actions or resources