Alright, let’s talk about SSH port forwarding, which is easily one of the coolest and most “wait, how is that even possible?” features SSH offers. Forget all that “SSH is just for a command line” nonsense. This is where you turn SSH into a digital skeleton key for your network, tunneling traffic through an encrypted pipe to places it was never meant to go. We have three main types: -L (local), -R (remote), and -D (dynamic). I’ll break them down, but first, the universal truth: Forwarding happens on the SSH client machine. Remember that. It’s the machine where you type the ssh command that does the magic.

Local Port Forwarding (-L)

This is the most common one. You’re telling your local machine (the client): “Hey, I want to listen on a port here. Whenever anything connects to it, shove that traffic through the SSH tunnel to a specific port on a specific machine that’s on the other side of this SSH server.”

The syntax is a bit of a mouthful, but you’ll get used to it:

ssh -L [local_bind_ip:]local_port:destination_host:destination_port user@ssh_server

Let’s make it real. Say you’re on your laptop (local_machine) and you need to access a database (db.internal) that’s locked away inside a private network. The only machine you can SSH to in that network is a bastion host (bastion.example.com). This is -L’s time to shine.

ssh -L 5432:db.internal:5432 myuser@bastion.example.com

Now, open another terminal on your local machine. You can point your database client to localhost:5432. Your laptop will see the connection, and SSH will faithfully forward it through the tunnel to bastion.example.com, which then connects to db.internal:5432 and sends the data back. To the database, the connection looks like it’s coming from bastion.example.com. Neat, right?

Pitfalls & Best Practices:

  • Privileged Ports: On Linux/Unix, ports below 1024 are privileged. If you want to bind local_port to one of those, you need to run the ssh command with sudo. It’s usually easier to just use a high-numbered port (like 55432) and avoid the hassle.
  • That local_bind_ip thing: By default, -L binds to localhost (127.0.0.1), meaning only programs on your same machine can use the tunnel. If you want another machine on your network to use your tunnel (e.g., your desktop forwarding to your laptop), you’d use 0.0.0.0 as the bind IP. Warning: This exposes the port on your network interface. Be sure you understand the security implications before you do this.
  • The destination is from the server’s perspective: destination_host is resolved by the SSH server (bastion.example.com), not by your client. If the server can’t resolve or reach db.internal, this fails.

Remote Port Forwarding (-R)

This one bends your brain a little. You’re telling the remote SSH server: “Hey, you should listen on a port. Whenever anything connects to you on that port, shove that traffic back through our tunnel to a specific port on my local machine.”

The syntax is its mirror-universe twin:

ssh -R [remote_bind_ip:]remote_port:destination_host:destination_port user@ssh_server

Why on earth would you do this? The classic use case is giving someone else access to a service running on your machine. Imagine you’re developing a web app on your laptop (local_machine:3000) behind a pesky NAT or firewall. Your colleague on the internet needs to see it. If you can SSH out to a publicly accessible server (public.server.com), you can punch a hole back to yourself.

ssh -R 8080:localhost:3000 myuser@public.server.com

Now, anyone who can reach public.server.com can go to http://public.server.com:8080 and see the web app running on your laptop. Your colleague is thrilled. Your sysadmin is… concerned. Which brings us to the biggest “questionable choice” with -R.

The Great RemoteForward Bind Address Debacle: For reasons, recent versions of OpenSSH default to binding the remote port to localhost on the server side. This means that even on public.server.com, you can’t access 8080 from the outside; only programs running on public.server.com can. It’s infuriatingly “secure by default” but often useless. To fix this, you often need to set GatewayPorts yes in the server’s sshd_config—a change requiring server-level permissions you probably don’t have. Always check if you can actually connect to the forwarded port from another machine before you assume it’s working.

Dynamic Port Forwarding (-D)

This is the lazy—and I mean that in the best way—person’s Swiss Army knife. You don’t specify a single destination. Instead, you set up a local SOCKS proxy server. You tell your applications (web browser, etc.) to use this proxy. The application says “I want to go to api.some-service.com:443,” and the SOCKS proxy says “Cool, I’ll send that request through the SSH tunnel and let the SSH server figure out how to connect to it.”

The syntax is blissfully simple:

ssh -D 1080 myuser@bastion.example.com

This creates a SOCKS5 proxy on your machine at localhost:1080. Configure your browser to use it (via its network settings), and now all your web traffic is routed through bastion.example.com. It’s fantastic for browsing on untrusted networks or getting around simplistic IP-based geo-blocking, as your traffic appears to originate from the SSH server.

The Fine Print:

  • It’s a proxy, not a VPN. Application support varies. CLI tools often need explicit configuration to use a SOCKS proxy (e.g., curl --socks5-hostname localhost:1080 https://ifconfig.me).
  • DNS can leak. If your application does DNS lookups itself instead of letting the SOCKS proxy handle it, those requests go out your normal interface. Using the socks5-hostname proxy type (instead of just socks5) forces the DNS lookup to happen on the remote side, which is what you usually want.

The power here is immense. You can chain these, combine them, and build entire ad-hoc networks. But with great power comes great responsibility—and the potential to create mind-bendingly complex networking puzzles for your future self to solve. Use them wisely.