Reverse SSH tunneling with AWS
I mostly blog about healthcare here, but I wanted to put up a quick tutorial on reverse tunneling for developers. There are plenty of explanations out there, but I wanted to write one that is simple, easy to understand, and repeatable.
What is reverse tunneling, and why is it useful for web developers?
SSH, or Secure Shell, is a cryptographic network protocol that allows us to set up secure connections between computers over an unsecure network, like the Internet. We can use it to run commands, forward ports, and transfer files. We can also use it to create a secure "tunnel" by wrapping unencrypted protocols (such as HTTP) in encryption.
Normal tunneling forwards a port on a local machine to a port on a remote machine. Reverse tunneling forwards a port on a remote machine to a port on a local machine.
This is useful because it allows us to direct third-party services and devices to a remote machine that is exposed to the Internet. Those requests will then be forwarded to our local machine. It's faster than deploying to a staging server.
I use it mostly to check how my iPhone and iPad are displaying CSS media queries, in addition to the Safari Developer tools.
How to set up a reverse tunnel
First off, you don't have to DIY. You can use Localtunnel or Forward for this. I like setting up my own because it's easy, responsive, and gives me a lot of control.
How easy is it? There are four steps:
Start a server, configured to allow reverse ssh tunneling
Configure our local ssh settings
Configure and start a server
Configure our local ssh settings
Configure server ssh settings and reload ssh config
Establish the reverse tunnel
I'm using OS X Terminal, OpenSSH and Amazon Web Services.
Step 1: Configure and Start a Server
We are going to launch an EC2 instance using the AWS Management Console interface. Go into the EC2 Mangement Console, go under Instances, and hit the "Launch Instance" button. You should see the Quick Launch Wizard:
You may need to create and download a keypair
I like Ubuntu Server 12.04.2 LTS
Hit "Continue." Next, let's edit the instance details.
I just use a t1.micro. If you want something beefier, select it under the Instance Details radio button.
Create a new Security Group
Call it something like "Reverse SSH Tunnel", add a description
Add a rule to allow access on port 22 (SSH) at source 0.0.0.0/0
Add another rule to allow access on port 3000 (or whatever port you want to forward traffic from) at source 0.0.0.0/0
Create the Security Group
Select that group and hit Save Details
Note that you can also launch an instance and edit the Security Group afterwards. The process is similar.
Optional: Assign an Elastic IP
When your instance launches, you will see it is assigned a default IP address, something like ec2-54-211-63-58.compute-1.amazonaws.com. To make that a little more manageable, and to persist an IP across starting/stopping an instance, let's assign an Elastic IP.
Under Network & Security, go to Elastic IPs
Allocate a New Address, in EC2
Associate the Address with your instance
Now when you select your instance, you should see something like this, with a nice static IP:
Step 2: Configure Local SSH Settings
Find the *.pem key you downloaded and copy it to your ~/.ssh folder. For me this was:
$ mv ~/Downloads/reverse-ssh-key.pem ~/.ssh
AWS requires us to set the permissions on our key so that only the owner may read it.
$ chmod 600 ~/.ssh/reverse-ssh-key.pem
To avoid having to pass in a long set of parameters every time we want to open the tunnel, we can edit the ~/.ssh/config file to include:
host 54.227.245.213
user ubuntu
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
CheckHostIP no
IdentityFile ~/.ssh/reverse-ssh-key.pem
ServerAliveInterval 120
Hit $ man ssh_config to read about these options.
Host is the IP address of the instance we fired up.
User should be whatever your OS prefers. If you use Amazon's Linux AMI, it will want you to log in as "ec2-user." Ubuntu will let you start a console as root, but will want you to be a normal user for other tasks.
Several options have to do with checking/managing keys. I'm ok here because we are just connecting to a specified IP. If you wanted to be paranoid then...you shouldn't be following this tutorial.
ServerAliveInterval pings the remote host every n seconds, to keep the connection alive. Not setting it will eventually result in a "Write failed: Broken pipe" error.
Save and open a new shell window to load the new config.
Step 3: Configure Server SSH Settings and Reload SSH Config
Check that you can ssh into your server now:
Finally, we have to configure the port binding on the instance. Get into the sshd_config file with write access and add "GatewayPorts yes".
ubuntu@ip-10-147-225-211:~$ sudo vim /etc/ssh/sshd_config
(In vim, arrow scroll to where you want to insert, hit “i” to insert, esc to end the insert, “:w” to save, “:q” to quit.)
If you need to not bind everything on the wildcard, you can set GatewayPorts to other things. ($ man ssh and $ man sshd_config it.)
Finally, reload the ssh configuration and exit the session:
ubuntu@ip-10-147-225-211:~$ sudo reload ssh
ubuntu@ip-10-147-225-211:~$ exit
Step 4: Establish the Reverse Tunnel
Now it's time for magic. Fire up a local server. I'm using port 3000. The syntax for reverse tunneling is:
$ ssh -R [bind_address:]port:host:hostport address
With GatewayPorts set to "yes" on the server, we can leave out the bind_address. So:
$ ssh -R 3000:localhost:3000 54.227.245.213
means that port 3000 on the remote server is to be forwarded to the given host and port on the local side. Remember, if you want to use a different remote port, you need to allow access in your EC2 Security Group settings.
You should be able to point a web browser at the specified port on your server (54.227.245.213:3000 here) and interact with your development environment.
The last thing I'm going to do is add an alias in my ~/.bashrc file:
alias devtunnel="ssh -NR 3000:localhost:3000 54.227.245.213"
-N runs the ssh session without executing a remote command. You can also run it with -f to send it to background.
Now, after I open a new shell window, I can start a new tunnel with: