├── LICENSE ├── ssh-ssm.sh └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lindsay P 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 | -------------------------------------------------------------------------------- /ssh-ssm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o nounset -o pipefail -o errexit 3 | 4 | SSH_DIR=$HOME/.ssh 5 | SSH_TMP_KEY=${SSH_DIR}/ssm-ssh-tmp 6 | 7 | die () { echo "[${0##*/}] $*" >&2; exit 1; } 8 | make_ssh_keys () { ssh-keygen -t rsa -N '' -f ${SSH_TMP_KEY} -C ssh-over-ssm; } 9 | clean_ssh_keys () { rm -f ${SSH_TMP_KEY}{,.pub}; } 10 | 11 | [[ $# -ne 2 ]] && die "usage: ${0##*/} " 12 | [[ ! $1 =~ ^i-([0-9a-f]{8,})$ ]] && die "error: invalid instance-id" 13 | 14 | if [[ $(basename -- $(ps -o comm= -p $PPID)) != "ssh" ]]; then 15 | exec ssh -o IdentityFile="${SSH_TMP_KEY}" -o ProxyCommand="$0 $1 $2" "$2@$1" 16 | elif pr="$(grep -sl --exclude='*-env' "$1" ${SSH_DIR}/ssmtool-*)"; then 17 | export AWS_PROFILE=${AWS_PROFILE:-${pr##*ssmtool-}} 18 | fi 19 | 20 | # get ssh key from agent or generate a temp key 21 | if ssh-add -l >/dev/null 2>&1; then 22 | SSH_PUB_KEY="$(ssh-add -L |head -1)" 23 | else 24 | [[ -f ${SSH_TMP_KEY}.pub ]] || make_ssh_keys 25 | trap clean_ssh_keys EXIT 26 | SSH_PUB_KEY="$(< ${SSH_TMP_KEY}.pub)" 27 | fi 28 | 29 | # command to put our public key on the remote server (user must already exist) 30 | ssm_cmd=$(cat </dev/null 2>&1" 36 | EOF 37 | ) 38 | 39 | # execute the command using aws ssm send-command 40 | command_id=$(aws ssm send-command \ 41 | --instance-ids "$1" \ 42 | --document-name "AWS-RunShellScript" \ 43 | --parameters commands="${ssm_cmd}" \ 44 | --comment "temporary ssm ssh access" \ 45 | --output text \ 46 | --query Command.CommandId) 47 | 48 | # wait for successful send-command execution 49 | aws ssm wait command-executed --instance-id "$1" --command-id "${command_id}" 50 | 51 | # start ssh session over ssm 52 | aws ssm start-session --document-name AWS-StartSSHSession --target "$1" 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssh-over-ssm 2 | Configure SSH and use AWS SSM to connect to instances. Consider git-managing your configs for quick setup and keeping users up-to-date and in sync. 3 | 4 | **NOTE:** [ssm-tool](https://github.com/elpy1/ssm-tool) has been moved to its own repo. 5 | 6 | ## Getting started 7 | Recently I was required to administer AWS instances via Session Manager. After downloading the required plugin and initiating a SSM session locally using `aws ssm start-session` I found myself in a situation where I couldn't easily copy 8 | a file from my machine to the server (e.g. using `scp`, `sftp`, `rsync` etc). After some reading of the AWS documentation I discovered it's possible to connect via SSH over SSM, solving this issue. You also get all the other benefits and functionality of SSH e.g. encryption, proxy jumping, port forwarding, socks etc. 9 | 10 | At first I really wasn't too keen on SSM but now I'm an advocate! Some cool features: 11 | - You can connect to your private instances inside your VPC without jumping through a public-facing bastion or instance 12 | - You don't need to store any SSH keys locally or on the server. 13 | - Users only require necessary IAM permissions and ability to reach their regional SSM endpoint (via HTTPS). 14 | - SSM 'Documents' are available to restrict users to specific tasks e.g. `AWS-PasswordReset` or `AWS-StartPortForwardingSession`. 15 | - Due to the way SSM works it's unlikely to find yourself blocked by network-level security, making it a great choice if you need to get out to the internet from inside a restrictive network :p 16 | 17 | ## Requirements 18 | - EC2 instances must have access to `ssm.{region}.amazonaws.com` on port `443` 19 | - [IAM instance profile allowing SSM access](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started-instance-profile.html) must be attached to EC2 instance 20 | - [SSM agent](https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-manual-agent-install.html) must be installed on EC2 instance 21 | - [AWS CLIv2](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) installed locally 22 | - [Session Manager plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) installed locally 23 | 24 | Existing instances with SSM agent already installed may require agent updates. 25 | 26 | ## How it works 27 | `ssh-ssm.sh` is a small bash script that performs some checks on execution and then runs two AWS commands: 28 | - `aws ssm send-command` (with SSM document `AWS-RunShellScript`) 29 | - `aws ssm start-session` (with SSM document `AWS-StartSSHSession`) 30 | 31 | This allows you to connect via SSH to instances over SSM without needing to manage SSH keys on remote servers. 32 | 33 | The difference between this and the `ProxyCommand` recommended in the [AWS documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started-enable-ssh-connections.html) is `ssh-ssm.sh` automates placing your local SSH public key on the remote server prior to initiating the SSH connection. Without this step your public key must exist on the server (under the correct user's directory) before you connect. 34 | 35 | The public key copied to the remote server is removed automatically after 15 seconds, allowing enough time for SSH authentication. 36 | 37 | ## Installation and Usage 38 | This tool is intended to be used in conjunction with `ssh`. It requires that you've configured your AWS CLI (`~/.aws/{config,credentials}`) properly. You can either use it as a replacement for the standard AWS `ProxyCommand` or spend some time planning and updating your SSH config. 39 | 40 | ### Listing and updating SSM instances 41 | First, we need to make sure the agent on each of our instances is up-to-date. You can use `aws ssm describe-instance-information` to list instances and `aws ssm send-command` to update them. Alternatively, use [ssm-tool](https://github.com/elpy1/ssm-tool) to list or update your instances: 42 | 43 | Check your instances 44 | ``` 45 | [elpy@testbox ~]$ AWS_PROFILE=int-monitor1 python3 ssm-tool 46 | instance id |ip |agent up-to-date |platform |name 47 | ------------------------------------------------------------------------------------------------------------------ 48 | i-0xxxxxxxxxxxxx3b4 |10.xx.xx.6 |False |Ubuntu |instance1 49 | i-0xxxxxxxxxxxxx504 |10.xx.xx.84 |False |Amazon Linux | 50 | i-0xxxxxxxxxxxxxfe9 |10.xx.xx.143 |False |CentOS Linux |instance8 51 | 52 | ``` 53 | 54 | Update all instances 55 | ``` 56 | [elpy@testbox ~]$ AWS_PROFILE=int-monitor1 python3 ssm-tool --update 57 | success 58 | 59 | [elpy@testbox ~]$ AWS_PROFILE=int-monitor1 python3 ssm-tool.py 60 | instance id |ip |agent up-to-date |platform |name 61 | ------------------------------------------------------------------------------------------------------------------ 62 | i-0xxxxxxxxxxxxx3b4 |10.xx.xx.6 |True |Ubuntu |instance1 63 | i-0xxxxxxxxxxxxx504 |10.xx.xx.84 |True |Amazon Linux | 64 | i-0xxxxxxxxxxxxxfe9 |10.xx.xx.143 |True |CentOS Linux |instance8 65 | ``` 66 | 67 | ### SSH configuration 68 | 69 | Now that all of our instances are running an up-to-date agent we need to update our SSH config (`~/.ssh/config`). 70 | 71 | #### The minimum required 72 | ``` 73 | # applies to all hosts and ensures our SSH sessions remain alive when idle 74 | Host * 75 | TCPKeepAlive yes 76 | ServerAliveInterval 30 77 | ConnectTimeout 10 78 | 79 | #------ 80 | # place any other/existing configuration here 81 | #------ 82 | 83 | Match Host i-* 84 | ProxyCommand ssh-ssm.sh %h %r 85 | IdentityFile ~/.ssh/ssm-ssh-tmp 86 | StrictHostKeyChecking no 87 | BatchMode yes 88 | ``` 89 | This enables you to connect via `ssh` using the appropriate username and instance-id e.g. `ssh ec2-user@`. You'll need to ensure AWS credentials are available in your environment, either with `AWS_PROFILE` or `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN`. 90 | 91 | #### Basic configuration example 92 | ``` 93 | Host confluence-prod.personal 94 | Hostname i-0xxxxxxxxxxxxxe28 95 | User ec2-user 96 | ProxyCommand bash -c "AWS_PROFILE=atlassian-prod ~/bin/ssh-ssm.sh %h %r" 97 | 98 | Host jira-stg.personal 99 | Hostname i-0xxxxxxxxxxxxxe49 100 | User ec2-user 101 | ProxyCommand bash -c "AWS_PROFILE=atlassian-nonprod ~/bin/ssh-ssm.sh %h %r" 102 | 103 | Host jenkins-master.personal 104 | Hostname i-0xxxxxxxxxxxxx143 105 | User centos 106 | ProxyCommand bash -c "AWS_PROFILE=jenkins-home ~/bin/ssh-ssm.sh %h %r" 107 | 108 | Match Host i-* 109 | IdentityFile ~/.ssh/ssm-ssh-tmp 110 | BatchMode yes 111 | ``` 112 | 113 | Above we've configured 3 separate instances for SSH access by specifying the username, instance-id and host to use for local commands i.e. `ssh {host}`. We've also hard-coded the `AWS_PROFILE` environment variable for the `ProxyCommand` 114 | so we don't need to manually provide credentials via tooling. This type of configuration is generally OK if you only have a few instances to work with. 115 | 116 | ### Testing/debugging SSH connections 117 | 118 | Show which config file and `Host` you match against, and the final command executed by SSH: 119 | ``` 120 | ssh -G confluence-prod.personal 121 | ``` 122 | 123 | Debug connection issues: 124 | ``` 125 | ssh -vvv user@host 126 | ``` 127 | 128 | For further informaton consider enabling debug for `aws` (edit ssh-ssm.sh): 129 | ``` 130 | aws ssm --debug command 131 | ``` 132 | 133 | Once you've tested it and you're confident it's all correct give it a go! Remember to place `ssh-ssm.sh` in `~/bin/` (or wherever you prefer), and ensure it's available in your `$PATH`. 134 | 135 | ### Example usage 136 | SSH: 137 | ``` 138 | [elpy1@testbox ~]$ aws-mfa 139 | INFO - Validating credentials for profile: default 140 | INFO - Your credentials are still valid for 14105.807801 seconds they will expire at 2020-01-25 18:06:08 141 | [elpy1@testbox ~]$ ssh confluence-prod.personal 142 | Last login: Sat Jan 25 08:59:40 2020 from localhost 143 | 144 | __| __|_ ) 145 | _| ( / Amazon Linux 2 AMI 146 | ___|\___|___| 147 | 148 | https://aws.amazon.com/amazon-linux-2/ 149 | [ec2-user@ip-10-xx-x-x06 ~]$ logout 150 | Connection to i-0fxxxxxxxxxxxxe28 closed. 151 | ``` 152 | 153 | SCP: 154 | ``` 155 | [elpy@testbox ~]$ scp ~/bin/ssh-ssm.sh bitbucket-prod.personal:~ 156 | ssh-ssm.sh 100% 366 49.4KB/s 00:00 157 | 158 | [elpy@testbox ~]$ ssh bitbucket-prod.personal ls -la ssh\* 159 | -rwxrwxr-x 1 ec2-user ec2-user 366 Jan 26 07:27 ssh-ssm.sh 160 | ``` 161 | 162 | SOCKS: 163 | ``` 164 | [elpy@testbox ~]$ ssh -f -NT -D 8080 jira-prod.personal 165 | [elpy@testbox ~]$ curl -x socks://localhost:8080 ipinfo.io/ip 166 | 54.xxx.xxx.49 167 | [elpy@testbox ~]$ whois 54.xxx.xxx.49 | grep -i techname 168 | OrgTechName: Amazon EC2 Network Operations 169 | ``` 170 | 171 | DB tunnel: 172 | ``` 173 | [elpy@testbox ~]$ ssh -f -NT -oExitOnForwardFailure=yes -L 5432:db1.host.internal:5432 jira-prod.personal 174 | [elpy@testbox ~]$ ss -lt4p sport = :5432 175 | State Recv-Q Send-Q Local Address:Port Peer Address:Port 176 | LISTEN 0 128 127.0.0.1:postgres *:* users:(("ssh",pid=26130,fd=6)) 177 | [elpy@testbox ~]$ psql --host localhost --port 5432 178 | Password: 179 | ``` 180 | 181 | SSH (with minimum required configuration): 182 | ``` 183 | [elpy@testbox ~]$ jumpbox=$(aws --profile atlassian-prod ec2 describe-instances --filters 'Name=tag:Name,Values=confluence-prod' --output text --query 'Reservations[*].Instances[*].InstanceId') 184 | [elpy@testbox ~]$ echo ${jumpbox} 185 | i-0fxxxxxxxxxxxxe28 186 | [elpy@testbox ~]$ AWS_PROFILE=atlassian-prod ssh ec2-user@${jumpbox} 187 | Last login: Sat Jan 25 08:59:40 2020 from localhost 188 | 189 | __| __|_ ) 190 | _| ( / Amazon Linux 2 AMI 191 | ___|\___|___| 192 | 193 | https://aws.amazon.com/amazon-linux-2/ 194 | [ec2-user@ip-10-xx-x-x06 ~]$ logout 195 | Connection to i-0fxxxxxxxxxxxxe28 closed. 196 | ``` 197 | --------------------------------------------------------------------------------