SSH Tunnel: Remote Port Forwarding

Local and remote SSH tunnels serve the same fundamental purpose: they make it possible to securely send data across an unsecured network. The implementation details are subtly different though. A local SSH tunnel acts like a secure bridge from a local machine to a remote server. It’s ideal for accessing services on the remote server which aren’t publicly exposed. Conversely, a remote SSH tunnel reverses this direction, forwarding traffic from the remote server back to a local machine (or another machine).

The critical distinction between the two is the direction of the connection between the remote and local machines.

In the previous post we saw how to set up port forwarding through a local SSH tunnel. In this post we’ll explore a complimentary approach using remote SSH tunnels.

We’ll look at three different setups that demonstrate the utility of a remote SSH tunnel:

Tunnel SSH to Another Port

Consider the three machines illustrated in the figure below. There are two networks, public and private, where the former is on the public internet, while the latter is not. The Local Host is on the public network and should be considered our local machine (equivalent to your laptop or desktop machine). The Remote Host is on the private network and is not directly accessible from Local Host. The SSH Server (public IP 18.170.229.50) straddles the public and private networks. It’s accessible from Local Host and can connect to Remote Host.

A remote SSH tunnel created from the SSH server back to the local host. The client in the SSH connection used to establish the tunnel is indicated by a grey background.
A remote SSH tunnel created from the SSH server back to the local host. The client in the SSH connection used to establish the tunnel is indicated by a grey background.

In the previous post we created an SSH tunnel via an SSH connection from the local host to the SSH server. Suppose now that I needed to make an SSH connection in the opposite direction: from the SSH server to the local host. However, although the local host has a public IP, suppose that it’s on a network where incoming connections on port 22 are not allowed, so I can’t simply SSH from the SSH server to the local host.

A remote SSH tunnel can solve the problem though.

From the local host create an SSH connection to the SSH server. This connection uses the -R flag to create a remote tunnel back from the SSH server (port 2200) to the local host (port 22). This means that traffic to port 2200 on the SSH server will be proxied to port 22 on the local host.

# Connect from to local host to SSH server and reverse forward port 2200 to port 22.
#
# 18.170.229.50 — public IP of the SSH server
#
ssh -R 2200:localhost:22 ubuntu@18.170.229.50

Now on the SSH server we can SSH to local host on port 2200.

# Connect from SSH server back to local host on port 2200.
#
ssh -p 2200 andrew@localhost
ssh -p 2200 andrew@127.0.0.1

In the above commands localhost does not refer to the local host. Rather this is localhost from the perspective of the SSH server.

💡 In order to connect via this mechanism a few things need to be in place:

  1. A valid username on local host (andrew) must be used rather than the generic ubuntu user (although it’s certainly possible that ubuntu is a valid username on local host!).
  2. The SSH public key for the ubuntu user on the SSH server must be in .ssh/authorized_keys on the local host. This ensures that the ubuntu user on SSH server is authorised to connect to the local host.
  3. The SSH server daemon needs to be installed and running on the local host (not always the case with non-server machines).
# Install SSHD.
sudo apt-get install openssh-server
# Start SSHD immediately.
sudo systemctl enable ssh --now

Tunnel to MySQL

Now let’s set up a remote tunnel to access MySQL on the remote host.

In the previous post we accomplished this via a local SSH tunnel initiated from the local host. Now we will initiate a remote SSH tunnel from the SSH server. The SSH connection will be from the SSH server to the local host and the tunnel will proxy data in the opposite direction from port 3306 on the local host (public IP 18.133.180.232) to port 3306 on the remote host (private IP 172.31.46.48). Unlike in the previous section, we’re assuming that the SSH server is able to create an SSH connection to local host on port 22.

ssh -R 3306:172.31.46.48:3306 ubuntu@18.133.180.232
#
# 18.133.180.232 — public IP of the local host
# 172.31.46.48 — private IP of the remote host
The SSH client on the SSH server establishes the remote tunnel from the local host to the remote host. The client in the SSH connection is indicated by a grey background.
The SSH client on the SSH server establishes the remote tunnel from the local host to the remote host. The client in the SSH connection is indicated by a grey background.

Now from the local host we can connect to MySQL on the remote host as follows:

mysql -h 127.0.0.1 -P 3306 -u root -p

As an aside, if MySQL were running on the SSH server rather than the remote host (unlikely but possible!) then you’d set up the remote SSH tunnel like this:

ssh -R 3306:localhost:3306 ubuntu@18.133.180.232

This is similar to the previous SSH tunnel: the entrance to the tunnel is still port 3306 on the local host but now the exit is port 3306 on the SSH server rather than port 3306 on the remote host.

APT via SSH Tunnel

If you have a server which is not connected to the public internet then how do you install or update packages? When you try and run sudo apt update you find that the APT archive is inaccessible.

Ign:1 http://eu-west-2.ec2.archive.ubuntu.com/ubuntu jammy InRelease
Ign:2 http://eu-west-2.ec2.archive.ubuntu.com/ubuntu jammy-updates InRelease
Ign:3 http://eu-west-2.ec2.archive.ubuntu.com/ubuntu jammy-backports InRelease
Ign:4 http://security.ubuntu.com/ubuntu jammy-security InRelease
Ign:1 http://eu-west-2.ec2.archive.ubuntu.com/ubuntu jammy InRelease
Ign:2 http://eu-west-2.ec2.archive.ubuntu.com/ubuntu jammy-updates InRelease
Ign:3 http://eu-west-2.ec2.archive.ubuntu.com/ubuntu jammy-backports InRelease
Ign:4 http://security.ubuntu.com/ubuntu jammy-security InRelease
Ign:1 http://eu-west-2.ec2.archive.ubuntu.com/ubuntu jammy InRelease
Ign:2 http://eu-west-2.ec2.archive.ubuntu.com/ubuntu jammy-updates InRelease
Ign:3 http://eu-west-2.ec2.archive.ubuntu.com/ubuntu jammy-backports InRelease
Ign:4 http://security.ubuntu.com/ubuntu jammy-security InRelease
Err:1 http://eu-west-2.ec2.archive.ubuntu.com/ubuntu jammy InRelease

Connect from the SSH server to the remote host, creating a remote tunnel to a suitable APT archive. You can check which archive should be used by simply running sudo apt update on the remote host (see sample output above).

ssh -R 8080:eu-west-2.ec2.archive.ubuntu.com:80 ubuntu@172.31.46.48
#
# 172.31.46.48 — private IP of the remote host

Now update /etc/apt/apt.conf.d/01proxy on the remote host:

Acquire::http::Proxy "http://localhost:8080";

Now you should be able to run APT freely on the remote host and all traffic will be proxied through the SSH tunnel. The proxy will send all traffic on port 8080 from the remote host to port 80 on the APT archive.

sudo apt update
sudo apt install docker.io

Conclusion: Local versus Remote Tunnels

SSH tunnels allow you to set up secure connections between machines. There are two types of tunnel, which differ in the direction of the connection:

  • local — a connection from a specific port on the local machine to a particular port on the remote machine; and
  • remote — a connection from a particular port on the remote machine to a specific port on the local machine.

Another way to think about this is that the local SSH tunnel has the tunnel in the same direction as the SSH connection, while the remote SSH tunnel has the tunnel in the opposite direction.

Which of these options you use will depend on the details of your networking setup and precisely what you are aiming to achieve.