├── .ssh └── config ├── CloudFormation template.yaml ├── LICENSE.txt ├── README.images ├── architecture-private.svg └── architecture-public.svg ├── README.md └── connect.sh /.ssh/config: -------------------------------------------------------------------------------- 1 | # SSH over Session Manager 2 | host i-* mi-* 3 | ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'" 4 | -------------------------------------------------------------------------------- /CloudFormation template.yaml: -------------------------------------------------------------------------------- 1 | # MIT License. See LICENSE.txt 2 | # Copyright (c) 2019 Maksim Aniskov MaksimAniskov@gmail.com 3 | 4 | --- 5 | AWSTemplateFormatVersion: "2010-09-09" 6 | 7 | Description: > 8 | This solution demonstrates SSH access to an EC2 instance 9 | that is not configured with EC2 Key Pair (= SSH private key), 10 | has no ports open to the public Internet, 11 | or even has no public IP address, NAT gateway, and so on. 12 | The solution leverages SSM Session Manager and EC2 Instance Connect. 13 | Read more at https://github.com/MaksimAniskov/aws-ssh-bastion-ssm 14 | 15 | Metadata: 16 | AWS::CloudFormation::Interface: 17 | ParameterGroups: 18 | - Label: 19 | default: "" 20 | Parameters: 21 | - SetupType 22 | - Label: 23 | default: EC2 Instance 24 | Parameters: 25 | - InstanceType 26 | - InstanceAmiId 27 | - Label: 28 | default: Network 29 | Parameters: 30 | - VpcCidr 31 | - SubnetCidr 32 | 33 | ParameterLabels: 34 | InstanceType: 35 | default: Instance Type 36 | SetupType: 37 | default: Mode 38 | VpcCidr: 39 | default: VPC CIDR 40 | SubnetCidr: 41 | default: Subnet CIDR 42 | InstanceAmiId: 43 | default: Instance AMI Id, or reference to SSM Parameter containing the value 44 | 45 | Parameters: 46 | 47 | SetupType: 48 | Type: String 49 | AllowedValues: 50 | - public 51 | - private 52 | Default: public 53 | Description: > 54 | 'public' creates a public subnet, and an EC2 instance with public IPv4 address. 55 | 'private' demonstrates access to instance in a private subnet, 56 | but this mode requires creating 4 VPC endpoints, which implies additional costs. 57 | See https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-create-vpc.html 58 | 59 | VpcCidr: 60 | Type: String 61 | Default: 10.0.0.0/16 62 | AllowedPattern: ((\d{1,3})\.){3}\d{1,3}/\d{1,2} 63 | 64 | SubnetCidr: 65 | Type: String 66 | Default: 10.0.0.0/17 67 | AllowedPattern: ((\d{1,3})\.){3}\d{1,3}/\d{1,2} 68 | 69 | InstanceType: 70 | Type: String 71 | Default: t3.nano 72 | 73 | InstanceAmiId: 74 | Type: AWS::SSM::Parameter::Value 75 | Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 76 | Description: See https://aws.amazon.com/blogs/compute/query-for-the-latest-amazon-linux-ami-ids-using-aws-systems-manager-parameter-store/ 77 | 78 | Conditions: 79 | IsPrivate: !Equals [ !Ref SetupType, private ] 80 | IsPublic: !Equals [ !Ref SetupType, public ] 81 | 82 | Resources: 83 | 84 | VPC: 85 | Type: AWS::EC2::VPC 86 | Properties: 87 | CidrBlock: !Ref VpcCidr 88 | EnableDnsSupport: true 89 | EnableDnsHostnames: !If [ IsPrivate, true, false ] 90 | Tags: 91 | - Key: Name 92 | Value: !Ref AWS::StackName 93 | 94 | IGW: 95 | Condition: IsPublic 96 | Type: AWS::EC2::InternetGateway 97 | Properties: 98 | Tags: 99 | - Key: Name 100 | Value: !Ref AWS::StackName 101 | 102 | GatewayAttachment: 103 | Condition: IsPublic 104 | Type: AWS::EC2::VPCGatewayAttachment 105 | Properties: 106 | InternetGatewayId: !Ref IGW 107 | VpcId: !Ref VPC 108 | 109 | Subnet: 110 | Type: AWS::EC2::Subnet 111 | Properties: 112 | VpcId: !Ref VPC 113 | CidrBlock: !Ref SubnetCidr 114 | MapPublicIpOnLaunch: !If [ IsPublic, true, false ] 115 | Tags: 116 | - Key: Name 117 | Value: !Ref AWS::StackName 118 | 119 | RouteTable: 120 | Type: AWS::EC2::RouteTable 121 | Properties: 122 | VpcId: !Ref VPC 123 | Tags: 124 | - Key: Name 125 | Value: !Ref AWS::StackName 126 | 127 | RouteTableAssociation: 128 | Type: AWS::EC2::SubnetRouteTableAssociation 129 | Properties: 130 | RouteTableId: !Ref RouteTable 131 | SubnetId: !Ref Subnet 132 | 133 | Route: 134 | Condition: IsPublic 135 | Type: AWS::EC2::Route 136 | Properties: 137 | RouteTableId: !Ref RouteTable 138 | DestinationCidrBlock: 0.0.0.0/0 139 | GatewayId: !Ref IGW 140 | 141 | VpcEndpointSG: 142 | Condition: IsPrivate 143 | Type: AWS::EC2::SecurityGroup 144 | Properties: 145 | GroupDescription: HTTPS from the subnet 146 | VpcId: !Ref VPC 147 | SecurityGroupIngress: 148 | - IpProtocol: tcp 149 | FromPort: 443 150 | ToPort: 443 151 | CidrIp: !Ref SubnetCidr 152 | Tags: 153 | - Key: Name 154 | Value: !Ref AWS::StackName 155 | 156 | VpcEndpointSsm: 157 | Condition: IsPrivate 158 | Type: AWS::EC2::VPCEndpoint 159 | Properties: 160 | ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm 161 | VpcEndpointType: Interface 162 | PrivateDnsEnabled: true 163 | VpcId: !Ref VPC 164 | SubnetIds: 165 | - !Ref Subnet 166 | SecurityGroupIds: 167 | - !GetAtt VpcEndpointSG.GroupId 168 | 169 | VpcEndpointSsmMessages: 170 | Condition: IsPrivate 171 | Type: AWS::EC2::VPCEndpoint 172 | Properties: 173 | ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages 174 | VpcEndpointType: Interface 175 | PrivateDnsEnabled: true 176 | VpcId: !Ref VPC 177 | SubnetIds: 178 | - !Ref Subnet 179 | SecurityGroupIds: 180 | - !GetAtt VpcEndpointSG.GroupId 181 | 182 | VpcEndpointEc2messages: 183 | Condition: IsPrivate 184 | Type: AWS::EC2::VPCEndpoint 185 | Properties: 186 | ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages 187 | VpcEndpointType: Interface 188 | PrivateDnsEnabled: true 189 | VpcId: !Ref VPC 190 | SubnetIds: 191 | - !Ref Subnet 192 | SecurityGroupIds: 193 | - !GetAtt VpcEndpointSG.GroupId 194 | 195 | # Required in order to support updating SSM Agent on the EC2 instance 196 | VpcEndpointS3: 197 | Condition: IsPrivate 198 | Type: AWS::EC2::VPCEndpoint 199 | Properties: 200 | ServiceName: !Sub com.amazonaws.${AWS::Region}.s3 201 | VpcEndpointType: Gateway 202 | VpcId: !Ref VPC 203 | RouteTableIds: 204 | - !Ref RouteTable 205 | 206 | InstanceRole: 207 | Type: AWS::IAM::Role 208 | Properties: 209 | AssumeRolePolicyDocument: 210 | Statement: 211 | - Action: sts:AssumeRole 212 | Effect: Allow 213 | Principal: 214 | Service: ec2.amazonaws.com 215 | ManagedPolicyArns: 216 | - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 217 | 218 | InstanceProfile: 219 | Type: AWS::IAM::InstanceProfile 220 | Properties: 221 | Roles: 222 | - !Ref InstanceRole 223 | 224 | Instance: 225 | Type: AWS::EC2::Instance 226 | Properties: 227 | ImageId: !Ref InstanceAmiId 228 | InstanceType: !Ref InstanceType 229 | SubnetId: !Ref Subnet 230 | IamInstanceProfile: !Ref InstanceProfile 231 | Tags: 232 | - Key: Name 233 | Value: !Ref AWS::StackName 234 | 235 | Outputs: 236 | InstanceId: 237 | Value: !Ref Instance 238 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Maksim Aniskov MaksimAniskov@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.images/architecture-private.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
AWS Cloud
AWS Cloud
VPC
VPC
Private subnet
Private subnet

EC2
<br>EC2

S3
[Not supported by viewer]
SSM
[Not supported by viewer]
EC2 Instance
EC2 Instance
③ Generate temporal SSH key
④ aws ec2-instance-connect send-ssh-public-key
[Not supported by viewer]
ec2messages
ec2messages
VPC Endpoints
VPC Endpoints
Update SSM Agent
if needed
[Not supported by viewer]
s3
s3
① Register as a
Managed Instance
① Register as a<br>Managed Instance
ssm
ssmmessages
ssm<br>ssmmessages
 ssh ProxyCommand
aws ssm start-session
[Not supported by viewer]
-------------------------------------------------------------------------------- /README.images/architecture-public.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Public subnet
Public subnet
AWS Cloud
AWS Cloud
VPC
VPC

EC2
<br>EC2

S3
[Not supported by viewer]
SSM
[Not supported by viewer]
EC2 Instance
EC2 Instance
③ Generate temporal SSH key
④ aws ec2-instance-connect send-ssh-public-key
[Not supported by viewer]
Update SSM Agent
if needed
[Not supported by viewer]
① Register as a
Managed Instance
① Register as a<br>Managed Instance
 ssh ProxyCommand
aws ssm start-session
[Not supported by viewer]
IGW
IGW
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This solution demonstrates how to access over SSH an EC2 instance 2 | having no SSH key installed on it, 3 | no ports open, 4 | even no public IP address configured. 5 | 6 | # How it works 7 | 8 | We leverage Amazon EC2 Instance Connect for 'injecting' a temporal SSH key we generate into the instance. 9 | 10 | For connecting the instance, we make use of AWS Systems Manager Session Manager configuring SSH client with a specific ProxyCommand. 11 | 12 | You can use this demo in two modes. The 'advanced' one creates a private subnet, 13 | which requires creating four VPC Endpoints in order to allow the EC2 instance to connect AWS services, 14 | namely SSM, EC2 and S3. 15 | 16 | > **Important!** Three of the endpoints are of Interface type powered by [AWS PrivateLink](https://aws.amazon.com/privatelink/). Creating Interface endpoints incurs **additional costs**. Please refer to [AWS PrivateLink pricing](https://aws.amazon.com/privatelink/pricing/). 17 | 18 | Another mode creates a public subnet and an EC2 instance in it with a public IP address. 19 | In this mode it still doesn't open port 22, or any other port. 20 | 21 | # References 22 | 23 | [AWS Systems Manager Session Manager for Shell Access to EC2 Instances](https://aws.amazon.com/blogs/aws/new-session-manager/), updated in August 2019, on AWS News Blog, 24 | and [Enable SSH Connections Through Session Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started-enable-ssh-connections.html) on AWS Systems Manager User Guide. 25 | 26 | [Introducing Amazon EC2 Instance Connect](https://aws.amazon.com/about-aws/whats-new/2019/06/introducing-amazon-ec2-instance-connect/) published on Jun 27, 2019 on What's New, 27 | and [Connect Using EC2 Instance Connect](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect-methods.html) on Amazon EC2 User Guide. 28 | 29 | [ProxyCommand](https://man.openbsd.org/ssh_config#ProxyCommand) 30 | on OpenSSH SSH client configuration files man page. 31 | 32 | # Architecture 33 | 34 | ## EC2 Instance in a Private Subnet 35 | ![Architecture: private subnet](README.images/architecture-private.svg "Architecture: private subnet") 36 | 37 | ## EC2 Instance in a Public Subnet 38 | ![Architecture: public subnet](README.images/architecture-public.svg "Architecture: public subnet") 39 | 40 | # How to deploy 41 | 42 | 1. If required, update aws cli to newer version supporting 43 | [ec2-instance-connect](https://docs.aws.amazon.com/cli/latest/reference/ec2-instance-connect/index.html) service. 44 | 45 | 1. Create an AWS CloudFormation stack providing ```CloudFormation template.yaml``` file as the template. 46 | 47 | 1. Make a note of the EC2 instance's id on the stack's outputs. 48 | 49 | 1. (Optional) Observe in AWS Systems Manager console if the instance appeared among Managed Instances. 50 | 51 | 1. Update SSM Agent on the instance if required. It [must be](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started-enable-ssh-connections.html) of version 2.3.672.0 or later. 52 |
You can use following command to update SSM Agent. 53 |
```aws ssm create-association --name AWS-UpdateSSMAgent --instance-id ...``` 54 | 55 | 1. Add content of SSH config snippet provided here to your ~/.ssh/config. You can do that with following command. 56 |
```cat ./.ssh/config >>~/.ssh/config``` 57 | 58 | # How to use 59 | 60 | Connect the instance with following command providing your instance id. 61 |
```./connect.sh ec2-user@i-xxxxxxxxxxxxx``` 62 | -------------------------------------------------------------------------------- /connect.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -lt 1 ] 4 | then 5 | cat >&2 << EOT 6 | Connects EC2 instance over SSH over SSM Sessions with an one-time key leveraging Amazon EC2 Instance Connect. 7 | 8 | Usage: $0 user-name@instance-id [ssh options ...] 9 | 10 | EOT 11 | exit 1 12 | fi 13 | 14 | IFS='@' 15 | read -ra params <<< "$1" 16 | userName=${params[0]} 17 | instanceId=${params[1]} 18 | if [ -z "$instanceId" ] 19 | then 20 | echo "Instance id not set" 21 | exit 2 22 | fi 23 | 24 | az=`aws ec2 describe-instances --instance-ids $instanceId --query "Reservations[0].Instances[0].Placement.AvailabilityZone" --output text` 25 | retVal=$? 26 | if [ $retVal -ne 0 ]; then 27 | exit $retVal 28 | fi 29 | 30 | keyFileName="ec2instconnect.$RANDOM" 31 | ssh-keygen -f $keyFileName -q 32 | 33 | aws ec2-instance-connect send-ssh-public-key --instance-id $instanceId --availability-zone $az --instance-os-user $userName --ssh-public-key file://$keyFileName.pub 34 | #>/dev/null 35 | retVal=$? 36 | if [ $retVal -ne 0 ]; then 37 | rm $keyFileName $keyFileName.pub 38 | exit $retVal 39 | fi 40 | 41 | shift 42 | ssh -i $keyFileName $* $userName@$instanceId 43 | 44 | rm $keyFileName $keyFileName.pub 45 | --------------------------------------------------------------------------------