What it is
An EC2 Instance Connect Endpoint (EIC Endpoint) is an endpoint you place in a subnet. It lets you open a TCP tunnel from your laptop, over the AWS API, to an instance's private IP. You need no public IP, no bastion host and no inbound rule from the internet. Access is controlled with IAM and security groups.
Typical uses: SSH to Linux (port 22), RDP to Windows (3389), or any other TCP port such as a database admin port.
Set it up
1. Security groups
Create one security group for the endpoint and reference it from the instances.
# Endpoint SG: allow outbound to the instance ports
EICE_SG=$(aws ec2 create-security-group --group-name eice-sg \
--description "EC2 Instance Connect Endpoint" --vpc-id vpc-0a1b2c3d \
--query GroupId --output text)
aws ec2 revoke-security-group-egress --group-id $EICE_SG \
--ip-permissions '[{"IpProtocol":"-1","IpRanges":[{"CidrIp":"0.0.0.0/0"}]}]'
aws ec2 authorize-security-group-egress --group-id $EICE_SG \
--ip-permissions '[{"IpProtocol":"tcp","FromPort":22,"ToPort":22,"UserIdGroupPairs":[{"GroupId":"sg-instances"}]},
{"IpProtocol":"tcp","FromPort":3389,"ToPort":3389,"UserIdGroupPairs":[{"GroupId":"sg-instances"}]}]'
# Instance SG: allow inbound from the endpoint SG
aws ec2 authorize-security-group-ingress --group-id sg-instances \
--protocol tcp --port 22 --source-group $EICE_SG
2. Create the endpoint
aws ec2 create-instance-connect-endpoint \
--subnet-id subnet-0private1 --security-group-ids $EICE_SG
aws ec2 describe-instance-connect-endpoints \
--query "InstanceConnectEndpoints[].{id:InstanceConnectEndpointId,state:State,subnet:SubnetId}" --output table
Wait for create-complete. By default the instance sees the endpoint's private IP as the source. With --preserve-client-ip the instance sees your real IP instead, and the instance security group must then allow your client IPs rather than the endpoint SG. Preserve-client-IP has restrictions, so check the current documentation before relying on it.
3. IAM
Users need permission to open the tunnel. Restrict by endpoint and port:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ec2-instance-connect:OpenTunnel",
"Resource": "arn:aws:ec2:eu-west-1:111122223333:instance-connect-endpoint/eice-0123456789abcdef",
"Condition": {
"NumericEquals": { "ec2-instance-connect:remotePort": "22" },
"NumericLessThanEquals": { "ec2-instance-connect:maxTunnelDuration": "3600" }
}
},
{
"Effect": "Allow",
"Action": ["ec2:DescribeInstances", "ec2:DescribeInstanceConnectEndpoints"],
"Resource": "*"
}
]
}
If you use aws ec2-instance-connect ssh without your own key, the user also needs ec2-instance-connect:SendSSHPublicKey on the instance.
Connect
SSH (Linux):
# Uses EC2 Instance Connect to push a temporary key (needs the EC2 Instance Connect package on the instance)
aws ec2-instance-connect ssh --instance-id i-0abc1234 --connection-type eice
# Or use your own key pair and OS user
aws ec2-instance-connect ssh --instance-id i-0abc1234 --connection-type eice \
--private-key-file ~/.ssh/prod.pem --os-user ec2-user
RDP (Windows) or any TCP port: open a local tunnel, then point your client at localhost.
aws ec2-instance-connect open-tunnel --instance-id i-0win5678 \
--remote-port 3389 --local-port 13389
# then connect your RDP client to localhost:13389
When it fails
| Error or symptom | Cause | Fix |
|---|---|---|
AccessDeniedException ... ec2-instance-connect:OpenTunnel | IAM policy missing, wrong endpoint ARN, or the port condition doesn't match | Fix the policy; check the remotePort condition matches the port you asked for |
| Tunnel opens, then times out / "Connection timed out during banner exchange" | Endpoint SG has no egress to the instance, or instance SG doesn't allow the endpoint SG | Check both rules; with preserve-client-IP, allow your client IP instead |
| "Permission denied (publickey)" | Temporary key not accepted (package missing) or wrong OS user | Use --private-key-file and the right user (ec2-user, ubuntu, ...) |
SendSSHPublicKey access denied | Using the push-key mode without that permission | Grant it, or use your own key |
| Endpoint stuck or "no endpoint found" | Endpoint in a different VPC from the instance | Create an endpoint in the instance's VPC; check the per-VPC limit |
| Session drops after an hour | Maximum tunnel duration reached | Reconnect; it is a hard cap, check the current limit |
| Instance unreachable from one subnet only | NACL blocks the endpoint subnet | Allow the ports and ephemeral return traffic in NACLs |
Quick reachability check from the CLI:
aws ec2 describe-security-groups --group-ids sg-instances \
--query "SecurityGroups[0].IpPermissions[?FromPort==\`22\`]"
Auditing who connected
Each tunnel is an OpenTunnel API call in CloudTrail, with the IAM identity, instance and port:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=OpenTunnel \
--max-results 20 --query "Events[].{time:EventTime,user:Username}" --output table
Prevent it next time
- One endpoint per VPC that needs admin access, created by your network baseline.
- Grant
OpenTunnelto groups (via IAM Identity Center permission sets), scoped to specific ports. - Remove port 22/3389 rules from
0.0.0.0/0everywhere once the endpoint works. - Keep SSM Session Manager as a second path for instances where the OS firewall or sshd is broken.