Expose a Local Port over a Remote VPS with SSH Remote Port Forwarding

Advertisement

Advertisement

Introduction

There are occassions when you want to expose a local port to the world so it can be accessed publicly on the internet.

For example, if you want to:

  • Share you local development environment publicly
  • Be able to receive webhooks from external services for your local development environment like Stripe webhooks
  • Expose a local database to the internet via a remote server

One option is to log in to your router, typically https://192.169.1.254/ or something similar, and configure port forwarding. That will let you tell the router to take incoming traffic for a specific port and send it your local computer. This can be annoying because you have to undo the change when you are done, which is easy to forget about, and you may not have access to your router with admin privileges at all. You can also run in to port conflicts.

Another option is to use a remote host, like a VPS rented from a service like Digital Ocean to expose your port to the internet. You can do this by using SSH port forwarding (tunneling) to securely forward a port from your local computer to a port on the Digital Ocean VPS. In turn, you can expose that port on the VPS to the internet.

This example is like an exercise in setting up your own ngrok service.

Steps

This example takes the following scenario:

  • You are running a local development web server that listens locally on your machine at localhost:8000.
  • You want to allow someone from the internet to connect to your web server, but you are behind a router and you can't configure port forwarding on the router.
  • You have a remote machine hosted on Digital Ocean with DNS configured as my-remote-host.com.

Given this scenario, we will look at using SSH tunnels to securely forward your local port to the remote server so anyone from the internet can reach it.

Set up the local service

First, you will need some service on your local machine that you want to expose publicly from another host. In this example we'll create a simple HTTP server using Python that will listen locally on localhost:8000.

# On your local machine, serve HTTP on 127.0.0.1:8000
python3 -m http.server --bind localhost 8000

See my tutorial on one-line HTTP servers for other ways to create quick HTTP servers with Ruby, Python, OpenSSL, PHP, and other tools. Of course, it doesn't have to be an HTTP server, it can be any TCP/IP service.

Initialize SSH port forwarding

While your HTTP server or other service is listening locally on your machine, use another terminal and open an SSH connection with the remote server. The one special thing about this SSH connection is that we want to specify a remote port forward so the remote server will expose a port that actually forwards back to your home machine.

# This will SSH to `my-remote-host.com` and while
# the session is open, the remote server will
# start listening on on port 9999 and any
# connection it recieves will get forward
# straight back to port 8000 on your local computer
ssh -R 9999:localhost:8000 my-remote-host.com

Verify port forwarding

On the remote server in your SSH shell, you can check to see what IP and ports are listening with netstat.

# On the remote server
netstat -ntl  # Show listening ports, verify port 9999 is listening

You may only see it listening only locally on 127.0.0.1:9999 instead of 0.0.0.0:9999. We'll look at how to reconfigure SSHD to open the ports publicly in the next section. Either way, you can use curl on the remote to check that the port is being forwarded locally at minimum.

# On the remote server
# Test the port forwarding. Should get back HTML
curl localhost:9999

Expose the forwarded port publicly

If your forwarded port is only listening locally on the remote server, that means SSHD has GatewayPorts set to no.

To expose the port to the internet you have a couple options:

  1. You can use a reverse proxy like nginx or Apache to listen on a public address like 0.0.0.0:80 and forward that to the local address 127.0.0.1:9999. (((see my nginx tutorial on reverse proxies)))
  2. You could use any other reverse proxy like http://mitmproxy.org/.
  3. Re-configure SSHD so it exposes forwarded ports on all interfaces instead of just localhost.

To reconfigure SSHD so it does not restrict forwarded ports to only listen locally, you can update your SSHD config file, usually /etc/ssh/sshd_config.

sudo vim /etc/ssh/sshd_config  # Set GatewayPorts yes
sudo systemctl restart sshd

Please keep in mind the ramifications of turning the GatewayPorts option to yes. If other system users are unaware of this, they could potentially expose sensitive data they did not intend to.

With GatewayPorts turned on, you can then hit the port from any host on the internet. For example if you were shelled in to my-remote-host.com, then someone on the internet could do:

curl http://my-remote-host.com:9999

Whenever the SSH connection is closed, so is the port tunnel so it's only temporary making it easy to tear down when you're done testing without any reconfiguration to your router.

SSH tunnel without the shell

Use the -N flag to start SSH without opening the shell. Add the -f flag to have SSH run in the background after starting. You will need to call kill manually if you run it in the background.

For example:

ssh -f -N -R 9999:localhost:8000 my-remote-host.com

Setup $HOME/.ssh/config file

You can setup a host entry in your $HOME/.ssh/config file so it includes a remote port forward. Here is an example entry:

Check out my SSH tips post for more tips on using SSH.

# $HOME/.ssh/config
Host mysandbox 
    HostName my-sandbox.com
    User myusername

    # RemoteForward <remote_port> <local_address>
    RemoteForward 9999 localhost:8000

You could then connect using the following command and it will include the port forward:

ssh -f -N mysandbox

A note about security

Although an SSH tunnel is secure between your local machine and the remote host, it has no control over what you do with that exposed port on the remote machine. There is no inherent authentication, authorization, or encryption on anything other than communication between your local machine and the remote host over the SSH connection. If you expose the port to the internet on the remote host, you are potentially exposing your home computer to the internet with no security whatsoever.

You will need to implement your own authentication like HTTP Basic Auth and your own encryption like SSL to make it more secure on the public internet. Just be aware that you could be exposing your local machine to the public internet unencrypted so be very conscious and careful with what you do.

Summary

To distill the steps necessary for this process, the summary is:

  1. Have a service lisetning on your local machine like an HTTP server.
  2. Connect to the remote host using SSH with the -R flag for remote port forwarding.

On the local machine, start an HTTP server on localhost:8000.

# On the local machine, start an HTTP server on localhost:8000
python3 -m http.server --bind localhost 8000

Then forward the port over SSH.

# Also on the local machine
ssh -R 9999:localhost:8000 my-remote-host.com

You can then hit the port locally while on the remote server.

# From the remote machine
curl http://localhost:9999

If SSHD is configured properly, then you can also from any host on the internet hit port 9999 on the remote server.

# From the internet (if SSHD configured with `GatewayPorts yes`)
curl http://my-remote-host:9999

References

Advertisement

Advertisement