A Practical Guide to SSH Tunnels: Local and Remote Port Forwarding | iximiuz Labs
Pangram verdict · v3.3
We believe that this document is fully human-written
AI likelihood · overall
HumanArticle text · 1,740 words · 6 segments analyzed
SSH is yet another example of an ancient technology that is still in wide use today. It may very well be that learning a couple of SSH tricks is more profitable in the long run than mastering a dozen Cloud Native tools or AI agent frameworks destined to become deprecated next quarter.One of my favorite parts of this technology is SSH Tunnels. With nothing but standard tools and often using just a single command, you can achieve the following:Access internal VPC endpoints through a public-facing EC2 instance.Open a localhost port of a remote development VM in the local browser.Expose any local server from a home/private network to the outside world.Tunnel your browser's debugging port to a remote sandboxed coding agent.And more 😍But despite the fact that I use SSH Tunnels daily, it always takes me a while to recall the right command. Should it be a Local or a Remote tunnel? What are the flags? Is it a local_port:remote_port or the other way around? So, I decided to finally wrap my head around it, and it resulted in a series of labs and a visual cheat sheet.The labs in this tutorial run on an attached playground with four hosts wired into three networks:internal - a device on the home network 192.168.0.0/24 (a homelab box, a NAS, a printer). Not reachable from the public network.local - your workstation. Sits on both the home 192.168.0.0/24 and the public 203.0.113.0/24 networks.remote - a public-facing bastion / gateway on the public 203.0.113.0/24 network, also connected to a private vpc 172.16.0.0/24.private - an internal-only service (a database, an OpenSearch cluster) on the vpc 172.16.0.0/24. Not reachable from the public network.You can ssh from local to remote by hostname or IP address - the local host key is already trusted on the remote machine:ssh remote ssh 203.0.113.30 Local Port ForwardingStarting from the one that I use the most.
Oftentimes, there might be a service listening on localhost or a private interface of a remote machine that I can only SSH to via its public IP. And I desperately need to access this port from my local machine. A few typical examples:Accessing a private remote database (MySQL, Postgres, Redis, etc) from your laptop using your favorite UI tool.Using your browser to access a web application exposed only to a private network.Accessing a container's port from your laptop without publishing it on the server's public interface.All of the above use cases can be solved with a single ssh command:ssh -L [local_addr:]local_port:remote_addr:remote_port [user@]sshd_addr The -L flag indicates we're starting a local port forwarding. What it actually means is:On your local machine, the SSH client will start listening on local_port (likely, on localhost, but it depends - check the GatewayPorts setting).Any traffic to this port will be forwarded to remote_addr:remote_port, reached from the remote machine you SSH-ed to.Here is what it looks like on a diagram:Pro Tip: Use ssh -f -N -L to run the port-forwarding session in the background.Lab 1: Using SSH Tunnels for Local Port Forwarding 👨🔬This lab reproduces the setup from the diagram above. The remote host runs a web server bound to 127.0.0.1:80, and we want to reach it from the local workstation.Because the service is bound to the loopback interface, it cannot be reached over the network. From the local host, try to hit the remote host's public address:curl 203.0.113.30:80 # remote.public curl: (7) Failed to connect to 203.0.113.30 port 80 after 0 ms: Could not connect to server But from the inside of the remote host, the very same service works just fine:curl localhost:80 Hello from the remote host (localhost-only service).
And here is the trick: back on the local host, bind the remote's localhost:80 to the local's localhost:8080 using local port forwarding:ssh -f -N -L 8080:localhost:80 203.0.113.30 Now you can access the web service on a local port of your workstation:curl localhost:8080 Hello from the remote host (localhost-only service). A slightly more verbose (but more explicit and flexible) way to achieve the same goal:ssh -f -N -L localhost:8080:localhost:80 203.0.113.30 # local remote via Local Port Forwarding with a Bastion HostIt might not be obvious at first, but the ssh -L command allows forwarding a local port to a remote port on any machine, not only on the SSH server itself. Notice how the remote_addr and sshd_addr may or may not have the same value:ssh -L [local_addr:]local_port:remote_addr:remote_port [user@]sshd_addr A remote SSH server used to access private destinations is usually called a bastion or jump host. This is how I visualize this scenario in my head:I often use the above trick to call endpoints that are accessible from the bastion host but not from my laptop (e.g., using an EC2 instance with private and public interfaces to connect to an OpenSearch cluster or any other service deployed fully within a VPC).Lab 2: Local Port Forwarding with a Bastion Host 👨🔬This lab reproduces the setup from the diagram above. The remote target service runs on the private host inside an improvised VPC network (172.16.0.40:80), and the former remote host acts as our public-facing bastion (jump host) that can reach it.The local workstation has no route into the VPC, so it cannot talk to the private host directly.
From the local host:curl --connect-timeout 3 172.16.0.40:80 # private.vpc curl: (28) Connection timed out after 3002 milliseconds The remote bastion, on the other hand, is connected to the VPC and can reach the private host. So, we forward a local port through the bastion straight to the private service. From the local host:ssh -f -N -L 8081:172.16.0.40:80 203.0.113.30 Checking that it works - still on the local host:curl localhost:8081 Hello from the private VPC host (172.16.0.40). Notice that the forwarding target (172.16.0.40) and the SSH server (203.0.113.30) are different machines. The bastion accepts the connection and opens the second hop to the private host on our behalf.A slightly more verbose (but more explicit and flexible) way to achieve the same goal:ssh -f -N -L localhost:8081:172.16.0.40:80 203.0.113.30 # local remote via Remote Port ForwardingAnother popular (but rather inverse) scenario is when you want to momentarily expose a local service to the outside world. Of course, for that, you'll need a public-facing ingress gateway server. And the good news is that any public-facing server with an SSH daemon on it can be used as such a gateway:ssh -R [remote_addr:]remote_port:local_addr:local_port [user@]gateway_addr The above command looks no more complicated than its ssh -L counterpart. But there is a pitfall...By default, the above SSH tunnel will allow using only the gateway's localhost as the remote address. In other words, your local port will become accessible only from inside the gateway server itself, which is most likely not what you actually need. For instance, I typically want to use the gateway's public address as the remote address to expose my local services to the public Internet.
For that, the SSH server needs to be configured with the GatewayPorts yes setting.Here is what remote port forwarding can be used for:Exposing a dev service from your laptop to the public Internet for a quick demo.Exposing your homelab to the public Internet (for arbitrary purposes).Tunneling your local browser's debugging port to a remote and/or sandboxed coding agent.Here is how the remote port forwarding can be visualized:Pro Tip: Use ssh -f -N -R to run the port-forwarding session in the background.Lab 3: Using SSH Tunnels for Remote Port Forwarding 👨🔬This lab reproduces the setup from the diagram above. The local workstation runs a web server bound to 127.0.0.1:80, and we want to expose it to the outside through the public-facing remote gateway.The service is bound to the loopback interface, so right now nobody but the local machine itself can reach it. Try accessing it from the remote machine:curl --connect-timeout 3 203.0.113.20:80 # local.public curl: (7) Failed to connect to 203.0.113.20 port 80 after 0 ms: Could not connect to server We want to expose it through the remote gateway and consume it from the private host. The remote gateway already has GatewayPorts yes in its sshd_config, so we can ask it to listen on all of its interfaces (0.0.0.0) and forward the traffic back to us. However, the local machine has to establish the tunnel first.From the local host, start the remote port forwarding:ssh -f -N -R 0.0.0.0:8080:localhost:80 203.0.113.30 # remote local via Now the local web service is published on the gateway's interfaces. Let's confirm it from a third machine - the private host, which can reach the remote gateway over the VPC:curl 172.16.0.30:8080 # remote.vpc Hello from your local workstation (localhost-only service).
Remote Port Forwarding to a Home or Private NetworkSimilar to local port forwarding, remote port forwarding has its own bastion or jump host mode. But this time, the machine with the SSH client (e.g., your dev laptop) plays the role of the jump host. In particular, it allows exposing ports of a home (or private) network reachable from your laptop to the outside world through a remote SSH server acting as an ingress gateway:ssh -R [remote_addr:]remote_port:local_addr:local_port [user@]gateway_addr Looks almost identical to the simple remote SSH tunnel, but the local_addr:local_port pair becomes the address of a device in the home network. Here is how it can be depicted on a diagram:I typically use my laptop as a thin client and the actual development happens on a remote server. Sometimes, such a remote server can reside in my home network and have no or restricted Internet access (for extra isolation). This is when I may want to rely on remote port forwarding to expose a service from a home server to the public Internet, using my laptop that can access both the internal dev server and the remote SSH server (ingress gateway) as a jump host.Lab 4: Remote Port Forwarding from a Home/Private Network 👨🔬This lab reproduces the setup from the diagram above. The service we want to expose runs on the internal host inside an isolated home network (192.168.0.10:80). Our local workstation can reach the home network and also has SSH access to the public-facing remote gateway, so it plays the role of a jump host.The local host can reach the internal service over the home network. From the local host:curl 192.168.0.10:80 # internal.home Hello from the internal home-network host (192.168.0.10). From the outside, though, the internal device is invisible. Try accessing it from the remote host:curl --connect-timeout 3 192.168.0.10:80 # internal.home curl: (28) Connection timed out after 3001 milliseconds The remote host has no route into the home network, so the request simply times out.