Guide · AWS Transfer Family · VPC Networking
AWS Transfer Family VPC Endpoint — Private SFTP Deployment, Security Groups, and Elastic IPs
AWS Transfer Family's VPC endpoint type deploys an NLB inside your VPC to serve SFTP, FTPS, or FTP traffic — giving you full control over security groups, subnet placement, and whether the server is reachable from the internet or only within the VPC. For MCP server developers, the VPC endpoint is the right choice when your SFTP partners need a static IP they can whitelist in their firewall, when your compliance requirements prohibit internet-facing endpoints without IP controls, or when your SFTP server needs to reach other VPC resources like a custom identity provider Lambda or a private RDS database. The critical networking tradeoff: PUBLIC endpoint is simpler (no VPC config, AWS manages routing) but offers no IP filtering; VPC endpoint is more powerful but requires allocating Elastic IPs per subnet, configuring security groups with port 22 rules, and understanding that the NLB does not terminate TLS — Transfer Family handles TLS at the application layer, not the NLB.
TL;DR
Create the Transfer Family server with endpoint-type VPC, specifying SubnetIds (one per AZ for high availability), SecurityGroupIds (a security group with inbound port 22 for SFTP), and optionally AddressAllocationIds (Elastic IPs to make the NLB internet-accessible with static IPs). Without Elastic IPs, the NLB is internal-only — reachable from within the VPC and via VPN/Direct Connect but not from the internet. With Elastic IPs, partners can whitelist the specific IPs in their firewalls. Security groups on the Transfer Family server control which source IPs reach the SFTP layer — they work like EC2 security groups and support CIDR-based allow rules on port 22.
VPC endpoint configuration
The VPC endpoint type provisions a Network Load Balancer in the specified subnets. You assign one subnet per Availability Zone for high availability — if one AZ's NLB node fails, clients reconnect to a healthy node in another AZ. The NLB registers Transfer Family's managed SFTP endpoint as targets automatically — you do not manage target registration or health checks.
# Allocate Elastic IPs (one per AZ/subnet)
EIP1=$(aws ec2 allocate-address --domain vpc \
--query 'AllocationId' --output text)
EIP2=$(aws ec2 allocate-address --domain vpc \
--query 'AllocationId' --output text)
# Create Transfer Family VPC server with Elastic IPs (internet-accessible)
aws transfer create-server \
--protocols SFTP \
--endpoint-type VPC \
--endpoint-details "{
\"VpcId\": \"vpc-0abc123\",
\"SubnetIds\": [\"subnet-0aaa\", \"subnet-0bbb\"],
\"SecurityGroupIds\": [\"sg-0transfer\"],
\"AddressAllocationIds\": [\"$EIP1\", \"$EIP2\"]
}" \
--identity-provider-type SERVICE_MANAGED \
--logging-role arn:aws:iam::123456789012:role/TransferLoggingRole
# Create Transfer Family VPC server without Elastic IPs (internal-only)
aws transfer create-server \
--protocols SFTP \
--endpoint-type VPC \
--endpoint-details '{
"VpcId": "vpc-0abc123",
"SubnetIds": ["subnet-0aaa", "subnet-0bbb"],
"SecurityGroupIds": ["sg-0transfer-internal"]
}' \
--identity-provider-type SERVICE_MANAGED
# Describe the server to get NLB DNS name and endpoint details
aws transfer describe-server \
--server-id s-0abc \
--query 'Server.EndpointDetails'
# Returns: SubnetIds, SecurityGroupIds, VpcId, VpcEndpointId, AddressAllocationIds
When you specify AddressAllocationIds, each Elastic IP is associated with a specific subnet's NLB node. Partners connecting to the server will appear to connect to one of these static IPs — SFTP clients typically use the NLB's DNS name, which resolves to one of the Elastic IPs. The Elastic IPs remain allocated to your account even if you delete the server — always release them explicitly with aws ec2 release-address to avoid ongoing charges.
Security group configuration
The security group attached to the Transfer Family VPC server controls inbound access to the SFTP endpoint. Transfer Family security groups work like EC2 security groups — you specify inbound rules by protocol, port range, and source CIDR or security group ID. For SFTP, allow port 22. For FTPS, allow port 21 and the passive data port range (49152–65535). The security group is applied to the NLB nodes, not to individual SFTP connections.
# Create a security group for the Transfer Family VPC server
SG_ID=$(aws ec2 create-security-group \
--group-name TransferFamilySFTP \
--description "Transfer Family SFTP server" \
--vpc-id vpc-0abc123 \
--query 'GroupId' --output text)
# Allow SFTP from specific partner CIDR ranges
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 22 \
--cidr 203.0.113.0/24 # Partner A datacenter CIDR
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 22 \
--cidr 198.51.100.0/24 # Partner B datacenter CIDR
# Allow SFTP from within the VPC (for internal clients)
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 22 \
--cidr 10.0.0.0/8
# For FTPS: also allow passive data port range
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 49152-65535 \
--cidr 203.0.113.0/24
# Verify security group rules
aws ec2 describe-security-groups \
--group-ids $SG_ID \
--query 'SecurityGroups[0].IpPermissions'
Transfer Family applies the security group at the NLB level — there is no "outbound" security group for the Transfer Family server itself (the NLB handles outbound to its targets internally). The key insight: NLB preserves the client's source IP address, so Transfer Family sees the actual client IP in logs and passes it to the custom identity provider Lambda via the sourceIp field. This is why VPC endpoint enables meaningful IP-based access control — the source IP is not NATed through the NLB.
Modifying VPC endpoint settings
Unlike endpoint type and identity provider type, most VPC endpoint settings can be modified after server creation. You can add or remove subnets, change security groups, and add or remove Elastic IP allocations using update-server. Adding a new subnet to a running server provisions a new NLB node in that AZ and makes the new Elastic IP (if any) immediately active — existing connections to other AZ nodes are not affected.
# Add a third subnet (third AZ) to an existing VPC server
EIP3=$(aws ec2 allocate-address --domain vpc \
--query 'AllocationId' --output text)
aws transfer update-server \
--server-id s-0abc \
--endpoint-details '{
"SubnetIds": ["subnet-0aaa", "subnet-0bbb", "subnet-0ccc"],
"AddressAllocationIds": ["eipalloc-111", "eipalloc-222", "'$EIP3'"]
}'
# Change security groups on a running server
aws transfer update-server \
--server-id s-0abc \
--endpoint-details '{
"SecurityGroupIds": ["sg-0transfer-new"]
}'
# Update only security groups without changing subnets or EIPs
# (other fields preserve existing values when omitted in update)
# Describe server to verify current endpoint configuration
aws transfer describe-server \
--server-id s-0abc \
--query 'Server.{State:State,EndpointType:EndpointType,EndpointDetails:EndpointDetails}'
When adding Elastic IPs, the AddressAllocationIds array must have exactly one entry per subnet in SubnetIds — the mapping is positional (first EIP → first subnet, etc.). If you add a third subnet, you must also allocate a third EIP and include all three in the update. Removing an EIP while keeping the subnet makes that subnet's NLB node private-only (internal VPC access, no public IP).
VPN and Direct Connect access
When a Transfer Family VPC server has no Elastic IPs (internal-only mode), it is accessible only from within the VPC, from peered VPCs, and from on-premises networks connected via VPN or Direct Connect. This is the recommended architecture for MCP servers that process files from internal batch systems — no public internet exposure, access controlled by VPN credentials and VPC routing.
# Internal-only Transfer Family server — accessed via VPN
# VPN clients connect to VPC, then SFTP to the NLB private DNS name
# Get NLB private DNS name for internal-only server
aws transfer describe-server \
--server-id s-0abc \
--query 'Server.EndpointDetails.VpcEndpointId'
# Returns VPC Endpoint ID — look up DNS name via:
aws ec2 describe-vpc-endpoints \
--vpc-endpoint-ids vpce-0abc123 \
--query 'VpcEndpoints[0].DnsEntries[0].DnsName'
# Returns: vpce-0abc123-us-east-1.transfer.us-east-1.vpce.amazonaws.com
# For peered VPCs, add a route in the peered VPC's route table
# pointing to the VPC endpoint's private IP addresses
# Check the NLB's IP addresses (for firewall rules in on-premises networks)
aws ec2 describe-network-interfaces \
--filters Name=description,Values="ELB net/transfer-*" \
--query 'NetworkInterfaces[*].{AZ:AvailabilityZone,IP:PrivateIpAddress}'
For Direct Connect and VPN access, your on-premises firewall must allow outbound port 22 to the NLB's private IP addresses. The NLB IP addresses are stable but not fully static — they can change during load balancer scaling events. For firewall rules in on-premises environments, use the NLB DNS name rather than hardcoded IPs, and configure your DNS resolver to refresh the NLB's IP addresses regularly (NLB DNS TTL is 60 seconds).
High-availability design patterns
Transfer Family VPC servers with NLBs in multiple AZs are resilient to single-AZ failures — the NLB routes new connections to healthy AZ nodes, and SFTP clients reconnect automatically after TCP timeout. However, in-flight SFTP transfers (files being uploaded at the time of an AZ failure) will fail and require the client to retry. Design your MCP processing pipeline to be idempotent — processing the same file twice should produce the same result or detect and skip duplicates.
# S3 object key structure for idempotent processing
# Include a content hash or unique ID in the key to detect duplicates
# Partner uploads: alice/2026-10-02/{uuid}/orders.csv
# Your Lambda checks if processed output already exists before processing
import boto3
import hashlib
s3 = boto3.client('s3')
def handler(event, context):
for record in event['Records']:
source_key = record['s3']['object']['key']
# e.g., "alice/2026-10-02/a1b2c3/orders.csv"
# Check if already processed
output_key = f"processed/{source_key}"
try:
s3.head_object(Bucket='mcp-processed', Key=output_key)
print(f"Already processed: {source_key} — skipping")
continue # Idempotent skip
except s3.exceptions.ClientError as e:
if e.response['Error']['Code'] != '404':
raise
# Process and write output atomically
process_and_write(source_key, output_key)
def process_and_write(source_key, output_key):
# Download, process, upload in single operation
# S3 PutObject is atomic — either the object exists or it doesn't
obj = s3.get_object(Bucket='mcp-file-intake', Key=source_key)
result = process(obj['Body'].read())
s3.put_object(Bucket='mcp-processed', Key=output_key, Body=result)
The idempotency check using HeadObject prevents double-processing when Transfer Family retries a failed upload or when your S3 event Lambda is invoked more than once for the same object (Lambda event delivery is at-least-once). For high-throughput file intake, use an SQS queue with message deduplication ID set to the S3 object key — SQS deduplication windows provide exactly-once delivery to your MCP processor.
Failure modes reference
| Failure | Symptom | Fix |
|---|---|---|
| Server stuck in ONLINE but connection refused | SFTP client times out or gets "Connection refused" on port 22 | Security group inbound rule is missing for port 22 from client's IP; verify security group attached to Transfer Family server (not VPC default SG) has the correct inbound rule |
| Elastic IPs not associated | Server created successfully but SFTP endpoint not reachable from internet | AddressAllocationIds count must match SubnetIds count; if you specified 2 subnets but 1 EIP, only one subnet's NLB node has a public IP — the other is internal-only |
| Server update fails with InvalidRequestException | Attempting to change EndpointType on existing server | EndpointType is immutable — PUBLIC cannot be changed to VPC and vice versa; create a new server with the correct type, migrate users, delete old server |
| Client IP shows NLB private IP in logs | Transfer Family logs show NLB IP (e.g., 10.0.x.x) instead of real client IP | VPC endpoint NLBs preserve client IP by default for TCP; if client IP is NLB IP, check if traffic passes through another proxy layer before reaching Transfer Family |
| Passive FTP data transfer fails | FTP LIST and RETR work on control channel but data transfer times out | Security group must allow inbound TCP 49152–65535 from the client CIDR; FTPS passive mode uses ephemeral ports for data channels separate from the control port 21 |