SSH tunnels for fun and profit

Ever worked on a project where your only permitted access to a remote resource was through a single whitelisted IP address? What about oauth workflows where your redirect URI is for a real server, not your dev machine? Unless you like editing your code on the remote server, it's a sad process of save, upload, test, edit, repeat. This is not ideal and we can't always choose the restrictions of services we want to integrate with.

In this post, I'll show you how to use SSH tunnels to ease all this pain on *nix based computers for the example above - implementing oauth where the API calls can only be made from a whitelisted IP address and the redirect URI is fixed to go to that same server. I'm on a mac in Australia, my server is ubuntu in New York and the 3rd party is elsewhere in the US. YMMV.

First, let's allow the API calls from my local machine to reach the 3rd party via my server:

sudo ssh -f -L 443:<3rd party domain>:443 <user>@<my server> -N  

That sets up the tunnel, but my code would have to access the API with localhost:443 which would cause SSL validation errors. A quick fix to the hosts file will fix that - add the following:

127.0.0.1 <3rd party domain>  

Now, when your code running on your dev machine accesses https://3rd-party-domain/endpoint, it will actually hit your machine which is now listening on port 443 and will forward those requests via your server to the 3rd party. NEAT!

Second, we need to use the real redirect_uri when returning from the oauth call. This is where a reverse SSH tunnel comes in.

sudo ssh -f -N -R 81:127.0.0.1:<local app port> <user>@<my server>  

Now, if you ssh into your server, you'll find you can:

curl localhost:81  

and your request will be routed back via the tunnel to your app on your dev machine.

On Ubuntu at least, accessing your server via the internet won't work here, for reasons I sadly couldn't resolve in a decent time frame - it's like the reverse tunnel only binds to localhost and no mucking with the ssh command seemed to fix that... so nginx to the rescue.

Long term, your server will end up with your application on it, but for development purposes, we can set up nginx to handle some of this for a pretty sweet dev setup.

Create a new entry in your nginx config (/etc/nginx/sites-enabled) on your server and try the following (the key here is the proxy_pass which will transparently pass the traffic to your app):

server {  
        listen 80 default_server;
        listen [::]:80 default_server ipv6only=on;

        root /usr/share/nginx/html;
        index index.html index.htm;

        server_name localhost;

        location / {
                proxy_pass http://localhost:81/;
        }
}
server {  
        listen 443;
        server_name localhost;

        root html;
        index index.html index.htm;

        ssl on;
        ssl_certificate /var/ssl/my-cert-inc-ca.pem;
        ssl_certificate_key /var/ssl/my-server.key;

        ssl_session_timeout 5m;

        ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
        ssl_prefer_server_ciphers on;

        location / {
                proxy_pass http://localhost:81/;
        }
}

Here, I've set up http and https to both redirect to the app on port 81 on this server, which from the previous step actually tunnels back to my dev machine running my app on port 3000.

So now, I can point my browser to my domain and step through the whole oauth process which is executing on my dev machine even though the implementation restrictions dictate the hostname of the redirect_uri and the validation calls must come from a non-local machine.

This whole workflow also allows you to show your friends and colleagues a working process on their machine (no more failures when they try to redirect to a localhost which isn't your machine).

Cover photo by Image Editor under the Creative Commons license