Guide · AWS PrivateLink · Cross-Account Service Access
AWS PrivateLink for MCP Servers — Expose as VPC Endpoint Service and Enable Cross-Account Access Without Peering
AWS PrivateLink lets you expose an MCP server running behind a Network Load Balancer as a named VPC Endpoint Service, so consumers in other VPCs — even in different AWS accounts — can connect to it via Interface endpoints without VPC peering, without routing CIDRs, and without touching the public internet. This is the right architecture when your organization runs MCP servers in a central shared-services account and needs to give access to multiple product-team accounts; when you're building an MCP server product and want enterprise customers to connect directly to your endpoint from within their own VPCs; or when consumer VPCs have overlapping CIDR blocks that make peering impossible. The fundamental difference from VPC peering: PrivateLink is unidirectional and service-scoped — the consumer can reach the specific service endpoint and nothing else in the provider VPC. No route tables are merged, no CIDR conflicts, no lateral movement risk.
TL;DR
Provider side: put the MCP server behind an NLB, then create a VPC Endpoint Service pointing at the NLB. Consumer side: create an Interface VPC endpoint pointing at the service name (format: com.amazonaws.vpce.us-east-1.vpce-svc-xxx). The consumer gets a private DNS name inside their VPC. For branded DNS, the provider can associate a private hosted zone so consumers get mcp.yourcompany.com instead of the vpce-xxx name. Connection acceptance can be auto-accept (for intra-org use) or manual (for external consumer access control).
Provider setup — NLB and VPC Endpoint Service
The MCP server must be behind a Network Load Balancer. Application Load Balancers are not supported as PrivateLink targets — you must use NLB. The NLB can be in the same VPC as the MCP server and does not need to be internet-facing. Create the endpoint service pointing at the NLB ARN; AWS then assigns a service name and manages the cross-VPC connectivity.
# 1. Create an NLB targeting the MCP server (if not already behind one)
NLB_ARN=$(aws elbv2 create-load-balancer \
--name mcp-server-internal \
--type network \
--scheme internal \
--subnets subnet-0private-a subnet-0private-b \
--query 'LoadBalancers[0].LoadBalancerArn' --output text)
# 2. Create a target group for the MCP server (HTTP on port 3000)
TG_ARN=$(aws elbv2 create-target-group \
--name mcp-server-tg \
--protocol TCP \
--port 3000 \
--vpc-id vpc-0abc123 \
--target-type ip \
--health-check-protocol TCP \
--health-check-port 3000 \
--query 'TargetGroups[0].TargetGroupArn' --output text)
# 3. Create NLB listener on port 443 forwarding to target group
aws elbv2 create-listener \
--load-balancer-arn $NLB_ARN \
--protocol TLS \
--port 443 \
--certificates CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abc \
--default-actions Type=forward,TargetGroupArn=$TG_ARN
# 4. Create the VPC Endpoint Service pointing at the NLB
SVC_ID=$(aws ec2 create-vpc-endpoint-service-configuration \
--network-load-balancer-arns $NLB_ARN \
--acceptance-required \
--query 'ServiceConfiguration.ServiceId' --output text)
# Get the service name that consumers will use
aws ec2 describe-vpc-endpoint-service-configurations \
--service-ids $SVC_ID \
--query 'ServiceConfigurations[0].ServiceName'
# Output: com.amazonaws.vpce.us-east-1.vpce-svc-0abc123
Set --acceptance-required if you want to manually approve each consumer connection request — appropriate for external customers. For internal organizational use across accounts in an AWS Organization, you can set --no-acceptance-required (auto-accept) and restrict which AWS principal ARNs are allowed to create endpoints using the modify-vpc-endpoint-service-permissions command. NLB health checks to the MCP server must pass for consumers to connect — an unhealthy target group takes the endpoint offline from the consumer's perspective.
Consumer setup — Interface endpoint creation
The consumer creates an Interface VPC endpoint specifying the provider's service name. AWS routes the endpoint's traffic through the AWS backbone to the provider's NLB. The consumer gets an endpoint-specific DNS name and, with private DNS correctly configured on the provider side, can optionally get a custom domain that resolves within their VPC.
# Consumer account: create an Interface endpoint to the provider's service
aws ec2 create-vpc-endpoint \
--vpc-endpoint-type Interface \
--service-name com.amazonaws.vpce.us-east-1.vpce-svc-0abc123 \
--vpc-id vpc-0consumer \
--subnet-ids subnet-0consumer-a subnet-0consumer-b \
--security-group-ids sg-0consumer-mcp \
--region us-east-1
# Get the endpoint DNS names
aws ec2 describe-vpc-endpoints \
--filters "Name=service-name,Values=com.amazonaws.vpce.us-east-1.vpce-svc-0abc123" \
--query 'VpcEndpoints[0].DnsEntries'
# Returns:
# - vpce-0xyz.vpce-svc-0abc123.us-east-1.vpce.amazonaws.com (endpoint-specific)
# - vpce-0xyz-us-east-1a.vpce-svc-0abc123.us-east-1.vpce.amazonaws.com (AZ-specific)
# Test connectivity from consumer VPC
curl https://vpce-0xyz.vpce-svc-0abc123.us-east-1.vpce.amazonaws.com/health
# or using custom DNS if provider has set up private hosted zone:
curl https://mcp.provider-company.com/health
The consumer's security group on the endpoint (sg-0consumer-mcp) controls what the endpoint allows — think of it as the "what can reach the endpoint ENI" layer. The consumer also needs an outbound rule from the application security group to the endpoint's security group. The NLB on the provider side does not expose source IPs to the MCP server — the source IP in the MCP server will be the NLB node's private IP, not the consumer's actual IP. If source IP attribution matters, use a Proxy Protocol v2 listener policy on the NLB.
Connection acceptance and allowlist management
When acceptance-required is true, every consumer endpoint creation request enters a "pendingAcceptance" state and must be explicitly approved. This is the right default for external-facing services — a consumer cannot connect until you approve their AWS account. For organizational use, use permissions to restrict who can create endpoints, and disable acceptance to reduce operational overhead.
# List pending connection requests
aws ec2 describe-vpc-endpoint-connections \
--filters "Name=vpc-endpoint-service-id,Values=$SVC_ID" \
"Name=vpc-endpoint-state,Values=pendingAcceptance" \
--query 'VpcEndpointConnections[*].{ID:VpcEndpointId,Account:VpcEndpointOwner,State:VpcEndpointState}'
# Accept specific endpoint connections
aws ec2 accept-vpc-endpoint-connections \
--service-id $SVC_ID \
--vpc-endpoint-ids vpce-0consumer1 vpce-0consumer2
# Reject a connection request
aws ec2 reject-vpc-endpoint-connections \
--service-id $SVC_ID \
--vpc-endpoint-ids vpce-0unwanted
# Grant permission to specific AWS principals to create endpoints (allowlist)
aws ec2 modify-vpc-endpoint-service-permissions \
--service-id $SVC_ID \
--add-allowed-principals \
arn:aws:iam::111122223333:root \
arn:aws:iam::444455556666:role/MCPConsumerRole
# Remove a principal from allowlist (they can no longer create new endpoints)
aws ec2 modify-vpc-endpoint-service-permissions \
--service-id $SVC_ID \
--remove-allowed-principals arn:aws:iam::111122223333:root
Revoking a principal from the allowlist does not terminate existing connections — it only prevents new endpoint creation. To terminate an active connection, use reject-vpc-endpoint-connections with the endpoint ID, which moves it to a "rejected" state and closes TCP connections within 30 seconds. Principals without explicit allowlist entries cannot create endpoints to your service even if they know the service name, as long as acceptance-required is true or the principal is not in the allowed list.
Private DNS for PrivateLink services
By default, consumers address the endpoint using the generated vpce-xxx.vpce-svc-xxx.region.vpce.amazonaws.com hostname. Providers can associate a private DNS name with the endpoint service so consumers who verify domain ownership can use a branded hostname instead. This requires ownership of a public DNS zone — AWS verifies the association with a TXT record.
# Associate a private DNS name with the endpoint service (provider side)
aws ec2 modify-vpc-endpoint-service-configuration \
--service-id $SVC_ID \
--private-dns-name mcp.provider-company.com
# AWS returns a verification TXT record to add to public DNS
aws ec2 describe-vpc-endpoint-service-configurations \
--service-ids $SVC_ID \
--query 'ServiceConfigurations[0].PrivateDnsNameConfiguration'
# Returns: {Name: "_some-token.mcp.provider-company.com",
# Value: "vpce-abc123verify", Type: "TXT", State: "pendingVerification"}
# After adding the TXT record to your public DNS zone, start verification
aws ec2 start-vpc-endpoint-service-private-dns-verification \
--service-id $SVC_ID
# Check verification status
aws ec2 describe-vpc-endpoint-service-configurations \
--service-ids $SVC_ID \
--query 'ServiceConfigurations[0].PrivateDnsNameConfiguration.State'
# "verified" when successful
# Consumer side: enable private DNS when creating the endpoint
aws ec2 create-vpc-endpoint \
--vpc-endpoint-type Interface \
--service-name com.amazonaws.vpce.us-east-1.vpce-svc-0abc123 \
--vpc-id vpc-0consumer \
--subnet-ids subnet-0consumer-a \
--security-group-ids sg-0consumer-mcp \
--private-dns-enabled
Once private DNS is verified and the consumer enables it on their endpoint, mcp.provider-company.com resolves to the endpoint ENI's private IP within the consumer VPC — no Route 53 private hosted zone setup needed on the consumer side. The consumer's existing Route 53 resolver handles it. If the consumer's VPC has split-horizon DNS or custom resolvers, ensure the custom resolver forwards the domain to Route 53 rather than intercepting it.
PrivateLink vs VPC peering — when to use each
| Factor | PrivateLink | VPC Peering |
|---|---|---|
| CIDR overlap | Works with overlapping CIDRs — no routing concern | Fails — peered VPCs cannot have overlapping CIDRs |
| Access scope | Consumer reaches specific service endpoint only — unidirectional | Full VPC-to-VPC route — any resource in either VPC can reach any other (scoped by security groups) |
| Cross-account | Native — service name is account-agnostic; principal allowlist controls access | Requires peering request/accept per account pair; does not work across AWS Organizations without explicit setup |
| On-premises access | Not directly accessible via VPN/Direct Connect from on-premises | Accessible via VPN/Direct Connect if peered VPC has connectivity |
| Data transfer cost | $0.01/GB + $0.01/hr per AZ per endpoint; NLB processing fees apply | $0.01/GB within the same region across different AZs (no charge within same AZ) |
| Transitive routing | Not needed — endpoint service is directly in consumer VPC's DNS | Not supported — A→B and B→C does not allow A→C |
Failure modes reference
| Failure | Symptom | Fix |
|---|---|---|
| Endpoint stuck in pendingAcceptance | Consumer endpoint created but traffic fails; no connection established | Provider must accept with accept-vpc-endpoint-connections; if acceptance-required=false check that consumer principal is in the allowed principals list |
| NLB health checks failing | Endpoint service exists but consumer connections get "endpoint unavailable" or timeout | NLB target group must show healthy targets; verify MCP server is running and the target group health check (TCP port) succeeds; unhealthy NLB takes PrivateLink service offline |
| Private DNS name not resolving in consumer VPC | Branded DNS name fails; consumers use vpce-xxx hostname successfully | Consumer must set private-dns-enabled=true when creating endpoint; provider DNS name must be in "verified" state; check with describe-vpc-endpoint-service-configurations |
| TLS certificate mismatch for custom DNS | SSL handshake error when consumer uses branded DNS name | NLB TLS listener certificate must include the branded DNS name (mcp.provider-company.com) as a SAN; ACM certificate in the same region as the NLB |
| Source IP shows NLB private IP, not consumer IP | MCP server logs show 10.x NLB IPs; cannot distinguish consumers | Enable Proxy Protocol v2 on the NLB target group and parse the proxy protocol header in the MCP server; or use the endpoint connection ID in access logs for correlation |