Skip to content
C Codeloom
Linux

Linux SSH Port Forwarding Tutorial

Learn local, remote, and dynamic SSH port forwarding with practical examples. Tunnel databases through bastions, expose local apps, and build SOCKS proxies safely.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • The difference between local, remote, and dynamic forwarding
  • How to reach a database hidden behind a bastion
  • How to expose a local dev server through a public host
  • Using -N, -f, and ssh config for long-lived tunnels
  • Common security and reliability pitfalls

Prerequisites

  • Basic SSH usage and command line familiarity

What and Why

SSH port forwarding turns an already-authenticated SSH connection into a secure TCP tunnel. Instead of opening firewall ports or running a VPN, you piggyback on SSH to reach services that are otherwise unreachable from your laptop. It is the default tool for hitting private databases through a bastion, demoing a local app to a teammate, or escaping a hostile coffee shop network.

You get three variants: local (-L), remote (-R), and dynamic (-D). They all share the same authentication and encryption, but they move traffic in different directions.

Mental Model

Think of SSH as a two-way pipe between your machine and the remote host. Forwarding adds an extra listening socket on one end and connects each accepted connection to a destination on the other end.

  • Local (-L): listen on my side, deliver on the remote side. “I want to reach something only the server can reach.”
  • Remote (-R): listen on the remote side, deliver on my side. “I want others to reach something only I can reach.”
  • Dynamic (-D): listen on my side as a SOCKS proxy. The remote side picks the destination per connection.

The destination address is always resolved on the side that delivers the traffic, not the side that listens. That single fact explains most surprises.

Hands-on Example

Suppose your team’s Postgres is in a private subnet, reachable only from bastion.example.com. You want to query it from your laptop with psql.

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

Now localhost:5433 on your laptop connects to db.internal:5432 from the bastion. In another terminal:

psql -h 127.0.0.1 -p 5433 -U app appdb

For a quick share of your local dev server on port 3000 through a public jump host:

ssh -R 8080:localhost:3000 user@public.example.com

Anyone hitting public.example.com:8080 reaches your laptop. You must set GatewayPorts yes in the server’s sshd_config if you want it bound on a public interface and not just loopback.

For a SOCKS proxy that routes browser traffic through the bastion:

ssh -D 1080 -N user@bastion.example.com

Point your browser at SOCKS5 127.0.0.1:1080.

Local (-L 5433:db.internal:5432):
laptop:5433  ==SSH==>  bastion  ->  db.internal:5432

Remote (-R 8080:localhost:3000):
public:8080  <=SSH==  laptop:3000
(clients hit public:8080)

Dynamic (-D 1080):
laptop:1080 (SOCKS)  ==SSH==>  bastion  ->  anywhere
Forwarding directions at a glance

For long-lived tunnels, use flags and an ssh config entry:

Host db-tunnel
  HostName bastion.example.com
  User app
  LocalForward 5433 db.internal:5432
  ServerAliveInterval 30
  ExitOnForwardFailure yes

Then ssh -N -f db-tunnel opens it in the background.

Common Pitfalls

  • Binding to all interfaces by accident: -L 5433:... binds to loopback. -L 0.0.0.0:5433:... binds publicly and may expose internal services to your local network.
  • Forgetting GatewayPorts: remote forwards bind to 127.0.0.1 on the server unless GatewayPorts yes is set.
  • DNS resolved on the wrong side: with -L, the destination is resolved on the bastion. localhost means the bastion’s loopback, not yours.
  • Dropped tunnels: NAT timeouts kill idle tunnels. Use ServerAliveInterval so SSH sends keepalives.
  • Port already in use: another tunnel or app holds the local port. Pick a different one rather than killing processes blindly.

Practical Tips

  • Use -N when you only want the tunnel, not a shell.
  • Combine -f with -N to background the tunnel after authentication.
  • Set ExitOnForwardFailure yes so the SSH process dies if the forward cannot bind, instead of silently leaving you without a tunnel.
  • For repeated workflows, put forwards in ~/.ssh/config so coworkers can copy your setup.
  • Prefer autossh for tunnels you need to survive flaky networks.

Wrap-up

Port forwarding is one of those tools that feels magical the first time and then becomes a daily habit. Remember the three directions, watch out for which side resolves the destination, and lock down what you expose. With an entry in your ssh config and a keepalive, you have a reliable, encrypted tunnel that beats almost any custom solution.