Home / AWS

AWSTroubleshootingIntermediate

EC2 Instance Connect Endpoint: SSH and RDP to private instances without a bastion, and fixing failed connections

Reach private EC2 instances over SSH or RDP through an EC2 Instance Connect Endpoint, and fix the security group, IAM and key problems behind failed tunnels.

Deepak Bhardwaj4 min read

Applies toAmazon EC2 Instance Connect Endpoint, Amazon Linux, Ubuntu, Windows Server, AWS CLI v2

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 OpenTunnel to groups (via IAM Identity Center permission sets), scoped to specific ports.
  • Remove port 22/3389 rules from 0.0.0.0/0 everywhere once the endpoint works.
  • Keep SSM Session Manager as a second path for instances where the OS firewall or sshd is broken.