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).
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
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. ) and modulo (e.g. ) 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 firstname.lastname@example.org
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
The above command will connect to
email@example.com and start forwarding the local port
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
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 firstname.lastname@example.org
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
localhost:8000 as follows:
ssh -N -R 4000:localhost:8000 email@example.com
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 firstname.lastname@example.org
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
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
--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.
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
B, and then from
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
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).
Share on Twitter and Facebook
Discussion of "SSH Tunnel - Local, Remote and Dynamic Port Forwarding"
If you have any questions, feedback, or suggestions, please do share them in the comments! I'll try to answer each and every one. If something in the article wasn't clear don't be afraid to mention it. The goal of these articles is to be as informative as possible.
If you'd prefer to reach out to me via email, my address is loading ..