SSH tunneling is an extremely useful feature of SSH that is very often googled, but less often understood enough to use without a reference. In this post I hope to explain it in such a way that you’ll have no confusion about when to use SHH’s local, remote, or even dynamic port forwarding. In its essence, port forwarding allows SSH to securely create an encrypted communication channel (a tunnel) between two computers on the network. We can use this channel to run commands on the remote server, expose a local port in a remote computer, expose a remote port on the local computer, or route traffic via a SOCKS proxy (more on this later).
Background
But first a tiny bit of background on how SSH works and why it’s secure. If you just want to get to the practical bits, feel free to skip this section and jump straight to Local Port Forwarding. You don’t need to understand it to use SSH tunnels in practice.
There are three types of encryption used at different stages: Diffie-Hellman, RSA, and AES (or other algorithms depending on configuration). If you’ve ever configured nginx and run into something called dhparam
or ssl_dhparam
, the dh
in there stands for the Diffie-Hellman algorithm, which is an amazingly simple algorithm to exchange a secret key over an insecure communication channel, without any prior knowledge. If you understand exponentiation (e.g. $2^{10} = 1024$) and modulo (e.g. $16 \mod 5 = 1$) you can understand Diffie-Hellman, as it’s only a few steps that could be done even on paper.
The issue with Diffiel-Hellman is that while you can exchange a secret, you don’t really know who you’re exchanging it with, and is vulnerable to the main-in-the-middle attack (someone could pretend to be the server and exchange the key with you instead). That’s where one more step comes in and the server uses its private key to sign a hash of some of the Diffie-Hellman parameters (check out section 8 of the RFC on what exactly gets signed), and the client then verifies the hosts signature using its public key. This is where SSH asks you to verify the host fingerprint, which is the fingerprint of its public key, and if you say yes, it means you’re validating the server truly is who they say they are (and not an attacker), and they key exchange can continue. If you always say yes
without verifying the host key, you’re vulnerable to a man-in-the-middle-attack.
After the server authenticity is confirmed, and the client and the server use Diffie-Hellman to negotiate a session key, which is then used to encrypt all of the traffic between them. You might be thinking why not use the already existing RSA keys (public/private keypair) of the client/server to encrypt the traffic? The answer is simple: asymmetric encryption is slow. Instead, SSH uses symmetric encryption (e.g. AES) to encrypt the traffic.
Lastly, the client is authorized against the server using it’s RSA keypair. The server will simply encrypt a random value using the client’s public key (taken from ~/.ssh/authorized_keys
) and send it over, the client verifies itself by being able to decrypt the message (because it owns the private key) and sends it back. If the values match, the client’s identity is verified and is authenticated now. (Note that there are a few technical details, such as hashing the values together with the session key, but those are not important for understanding the overall flow.)
Local Port Forwarding
The first forwarding mode we’ll look at is local port forwarding with the -L
flag. It’s called local because it allows us to forward connections from a local port to a different port on another computer on the network, using a secure SSH connection.
Say that you have a database (e.g. PostgreSQL) running on a server example.com
on port 5432
. The server is configured in such a way that only the SSH port 22
is open, and thus you can’t connect directly via psql -h example.com -p 5432
. You could SSH to the server and run psql -h localhost -p 5432
on there, but what if you wanted to use a GUI client for the database, and connect to the server directly?
With SSH you can simply forward an arbitrary local port, say 4000
, to the port 5432
on the server, but in such a way that the connection to 5432
would come as if from inside the server, and thus would be allowed. To do this we run:
ssh -N -L 4000:localhost:5432 [email protected]
We’ll use the -N
flag with all commands, which tell SSH to not start a shell (or execute a given command) and only forward ports. Personally I find this useful to distinguish which SSH session I use for forwarding and which ones might be just regular shell connections. You can of course forward while starting a remote shell (just omit the -N
flag).
The above command will connect to [email protected]
and start forwarding the local port 4000
to localhost:5432
on the server. This means we can now run psql -h localhost -p 4000
, and as psql
establishes a connection to localhost
on port 4000
, SSH will securely forward the connection to example.com
, where it connects to localhost:5432
. This way psql
doesn’t even know it’s connecting to a database running on a far away server.
One interesting tip is that we could forward to something else than localhost
on example.com
. Say that we have a second host named foobar.org
, which is accessible only from the example.com
server, but not accessible from your machine.
ssh -N -L 4000:foobar.org:5432 [email protected]
This way a connection to localhost:4000
on your machine would get forwarded through example.com
to connect to foobar.org
on port 5432
. In theory, you could also use this to bypass a firewall blocking direct connections from your computer, but dynamic port forwarding solves this problem more naturally by creating a SOCKS proxy (as we’ll see shortly).
Remote Port Forwarding
While local forwarding allows us to forward local connections to a remote port, with remote port forwarding we can accept connections on a remote server, and forward those to a local port on our machine. Say that we have a folder we want to share with a friend via our example.com
server, but we don’t want to copy the files over. We could start up a web server using python
$ python -m http.server
which creates a simple HTTP server on port 8000
that servers files from the current directory. We could then use SSH to remotly forward port 4000
on the server example.com
to localhost:8000
as follows:
ssh -N -R 4000:localhost:8000 [email protected]
Now you can tell your friend to go to http://example.com:4000
, and SSH will accept his connection, and forward it to your computer to localhost:8000
. There is one small catch, and that forwarding ports like this requires you to edit the configuration of SSH on the server. Specifically, you need add (or edit) GatewayPorts yes
to /etc/ssh/sshd_config
and restart the SSH daemon (via sudo systemctl restart sshd
), otherwise SSH won’t allow you to use this form of port forwarding.
Services like ngrok.com basically give you a fancy UI to remote port forwarding. They give you a CLI which you can run to make a local port available on the internet via a publically accesible subdomain. If you have your own server (which you can get for free on AWS/GCP, or for a few dollars per month on many providers), you can do exactly the same thing with a single command with SSH using remote port forwarding without any limitations, and save yourself some money :)
Dynamic Port Forwarding
The last type of forwarding is called dynamic port forwarding, which is perhaps a slightly confusing name, because the way you use it is different from the two previous forwarding mechanisms. With dynamic port forwarding you only specify the local port to bind to using the -D
parameter, and SSH will then determine where to forward connections based on the SOCKS protocol. The way this works is that SSH creates a SOCKS server which acts as a proxy which you can use in other applications.
Let’s say you still have the example.com
server with the SSH port open. It is also part of a private network with other servers on it, say a website private.example.com
, which is not accesible directly from the internet, but is accesible from example.com
. First we connect to example.com
with dynamic port forwarding:
ssh -N -D 5000 [email protected]
To connect to private.example.com
we need to configure the web browser to use our SOCKS proxy. In Firefox this can be done with Network Settings -> Manual proxy configuration -> SOCKS host
and select SOCKS v5
and set SOCKS Host
to localhost
and Port
to 5000
. You can also check Proxy DNS when using SOCKS v5
to resolve DNS using your SOCKS proxy, instead of resolving the hostname on your machine prior to making the request.
After this, you can just press OK
and type private.example.com
in the address bar and hit enter, and SSH will do the rest. Specifically it will connect to localhost:5000
via the SOCKS protocol and forward your request via the server to the website private.example.com
. In practice, this is as if you used a VPN to connect to the private network. The downside is you need to configure your browser (and any other program) which you want to connect via the proxy. It doesn’t connect your whole computer inside the network as a VPN program could, but this could also be considered a benefit if you just want to access something in isolation.
If you’re using Chrome (or Chromium), you can use this nifty one-liner to start a new instance with the SOCKS configuration pre-filled:
$ chromium --proxy-server=socks://localhost:5000 \
--user-data-dir=/tmp/foo
The --proxy-server=socks://localhost:5000
option does exactly what it says, it sets the SOCKS proxy configuration option. The --user-data-dir
option is a nice addition, because this way you could have a completely separate user profile for the proxied browser.
As a final note, you can use dynamic port forwarding to do things like access a website avaialble only in a specific country if you have a server example.com
which is hosted in that country. Or you could connect to websites on your company’s private network as long as you can SSH to any server on the network.
Conclusion
We covered three ways of port forwarding:
- Local port forwarding used for tunneling local connections to a port on a remote server.
- Remote port forwarding used for tunneling remote connections to a port on a local server.
- Dynamic port forwarding used for creating a TCP proxy via a remote host.
The given examples only scratch the surface of possible use cases. There are many cases where local or remote port forwarding can be useful during debugging multi-server architectures. You could even create multi-hop SSH tunnels where you tunnel from A
to B
, and then from B
to C
, e.g.
ssh -L 9999:host2:1234 -N host1
You can even use the first ssh
command to run ssh
on the remote host and create a second tunnel as the first one is created
ssh -L 9999:localhost:9999 host1 ssh -L 9999:localhost:1234 -N host2
which is not only cool, but also creates a secure SSH tunnel from host1
to host2
as opposed to the first method which does not (see this answer on SuperUser for more interesting examples.
Most importantly, play around and experiment with SSH when you get a chance! While not every combination of tunnels might be the best solution to your problem, there were certainly many times where knowing how to solve a problem using SSH tunnels saved me hours of otherwise tedious work (usually involving moving stuff around between servers).