DEV Community

dpm_bush
dpm_bush

Posted on Originally published at sshflow.com

SSH Port Forwarding Explained: Local, Remote, and Dynamic Tunnels

SSH port forwarding — also called SSH tunneling — forwards traffic for another service through an already-encrypted SSH connection.

If you've ever needed to access a database that only listens on localhost, expose a local development server through a remote machine, or route an application's traffic through an SSH host, port forwarding is usually the simplest way to do it.

There are three types:

Type Flag What it does
Local forwarding -L Forwards a port on your machine through the SSH server to another destination
Remote forwarding -R Forwards a port on the SSH server back to your machine
Dynamic forwarding -D Turns the SSH connection into a SOCKS proxy

Let's look at each one.

What is SSH port forwarding?

A normal SSH connection:

ssh user@host
Enter fullscreen mode Exit fullscreen mode

opens an interactive shell on the remote machine.

But SSH can carry more than terminal traffic over the same encrypted connection.

Port forwarding uses that connection to carry traffic from another service — for example:

  • a database connection
  • an internal web dashboard
  • a development server
  • application traffic through a SOCKS proxy

From the application's point of view, it usually connects to an ordinary local port. SSH handles transporting that traffic through the encrypted connection to its actual destination.

This is different from router port forwarding.

Router-level port forwarding changes NAT or firewall rules so incoming internet traffic reaches a device on a network.

SSH port forwarding happens entirely inside an existing SSH connection.


Local port forwarding with -L

Local forwarding is probably the most common type of SSH tunnel.

The syntax is:

ssh -L local_port:destination_host:destination_port user@ssh_server
Enter fullscreen mode Exit fullscreen mode

Where:

  • local_port is the port opened on your machine
  • destination_host:destination_port is the destination as seen from the SSH server
  • user@ssh_server is the server carrying the tunnel

Example: connect to a remote MySQL database

Imagine MySQL is running on a server but only listening on localhost:3306.

Instead of exposing MySQL directly to the internet, create a tunnel:

ssh -L 3306:localhost:3306 deploy@db-host.example.com
Enter fullscreen mode Exit fullscreen mode

Now point your local MySQL client at:

localhost:3306
Enter fullscreen mode Exit fullscreen mode

Your client behaves as if MySQL were running locally.

In reality the path looks like this:

Your MySQL client
      |
localhost:3306
      |
      | SSH tunnel
      v
db-host.example.com
      |
localhost:3306
      |
     MySQL
Enter fullscreen mode Exit fullscreen mode

The database port never needs to be publicly exposed. Only SSH does.

The same pattern works for Redis, internal dashboards, monitoring tools, and other services that aren't supposed to listen on a public interface.

A localhost gotcha

If the tunnel appears to work but your application hangs or gets refused, try 127.0.0.1 instead of localhost.

On some systems, localhost may resolve to the IPv6 loopback address ::1 first while the forward or destination service is listening only on IPv4.

Using 127.0.0.1 explicitly can avoid that mismatch.


Remote port forwarding with -R

Remote forwarding works in the opposite direction.

Instead of opening a port on your machine, it opens a port on the SSH server and forwards connections back through SSH to your local machine.

Syntax:

ssh -R remote_port:local_host:local_port user@ssh_server
Enter fullscreen mode Exit fullscreen mode

Example: expose a local development server

Suppose you're running an application locally on port 3000:

localhost:3000
Enter fullscreen mode Exit fullscreen mode

Create this tunnel:

ssh -R 8080:localhost:3000 user@ssh_server
Enter fullscreen mode Exit fullscreen mode

Connections to port 8080 on the SSH server are now forwarded through the SSH connection to port 3000 on your machine.

The path is essentially:

SSH server :8080
      |
      | SSH tunnel
      v
Your machine :3000
Enter fullscreen mode Exit fullscreen mode

By default, the remotely forwarded port is typically bound to loopback on the SSH server, meaning it can be reached from the server itself rather than automatically being exposed to the wider network.

This direction is also the foundation of an SSH reverse tunnel.

It's particularly useful when a machine:

  • has no public IP
  • sits behind NAT
  • sits behind a firewall
  • can make outbound SSH connections but can't accept inbound ones

For persistent reverse tunnels, there are additional things to consider such as GatewayPorts, restricted SSH keys, and automatic reconnection.


Dynamic port forwarding with -D

Local and remote forwarding both map a specific port to a specific destination.

Dynamic forwarding is different.

It turns SSH into a SOCKS proxy.

Start one with:

ssh -D 1080 user@ssh_server
Enter fullscreen mode Exit fullscreen mode

SSH now exposes a SOCKS proxy on:

localhost:1080
Enter fullscreen mode Exit fullscreen mode

Applications that support SOCKS can use that proxy and route their traffic through the SSH server.

For example:

curl -x socks5h://localhost:1080 https://example.com
Enter fullscreen mode Exit fullscreen mode

Unlike -L, you don't specify one destination when creating the tunnel.

The application decides where it wants to connect, and the SSH server makes the outbound connection on its behalf.

This is useful when you want to:

  • access multiple internal services through a jump host
  • route one application's traffic through a remote server
  • make requests originate from a trusted network location
  • avoid creating a separate forward for every service

Keeping an SSH tunnel alive

A tunnel only exists while its SSH connection is alive.

A few options make tunnel-only connections more practical.

-N

Don't execute a remote command:

ssh -N -L 3306:localhost:3306 user@server
Enter fullscreen mode Exit fullscreen mode

This is useful when the connection exists only for forwarding.

-f

Background SSH after authentication:

ssh -f -N -L 3306:localhost:3306 user@server
Enter fullscreen mode Exit fullscreen mode

SSH keepalives

You can also make SSH detect dead connections:

ssh -f -N \
  -L 3306:localhost:3306 \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  deploy@db-host.example.com
Enter fullscreen mode Exit fullscreen mode

ServerAliveInterval=30 makes the client periodically send a message through the encrypted SSH connection.

ServerAliveCountMax=3 makes SSH exit after several unanswered messages.

One important distinction: this detects a dead connection. It does not reconnect automatically.

If you need an unattended tunnel that reconnects after failure, a tool such as autossh can handle the reconnect loop.


Put frequently used tunnels in ~/.ssh/config

If you use the same tunnel regularly, you don't need to remember a giant command.

For example:

Host db-tunnel
  HostName db-host.example.com
  User deploy
  LocalForward 3306 localhost:3306
  ServerAliveInterval 30
  ServerAliveCountMax 3
Enter fullscreen mode Exit fullscreen mode

Now:

ssh db-tunnel
Enter fullscreen mode Exit fullscreen mode

opens the configured connection and forward.

The same SSH config can also contain options such as IdentityFile, ProxyJump, ports, usernames, and other per-host settings.


Is SSH tunneling secure?

The traffic inside an SSH tunnel inherits the encryption and authentication of the SSH connection.

But the way you expose the ends of the tunnel still matters.

Be careful with GatewayPorts

A remote forward created with -R normally isn't automatically exposed to everyone on the network.

Changing GatewayPorts can allow remotely forwarded ports to bind beyond loopback.

That can be intentional, but it can also expose a service much further than expected.

Prefer loopback when possible

There's an important difference between:

127.0.0.1
Enter fullscreen mode Exit fullscreen mode

and:

0.0.0.0
Enter fullscreen mode Exit fullscreen mode

A forwarded port bound to 127.0.0.1 is reachable only from the same machine.

Binding to 0.0.0.0 can make it reachable from other machines on the network as well.

Don't broaden the bind address unless you actually need to.

Close tunnels you no longer need

It's easy to create a tunnel for a quick task and forget about it.

A forgotten tunnel to a production database is still an access path.

Treat tunnels like any other connection to a sensitive service: know why they're running and close them when they're no longer needed.


SSH tunnel vs VPN vs exposing a port

These solve related problems, but they're not interchangeable.

SSH tunnel VPN Public port
Scope Usually one service, or selected app traffic with -D Entire network One publicly reachable service
Setup Usually one SSH command VPN server + client configuration Firewall/NAT configuration
Best for Reaching a few services through an SSH host Ongoing access to a private network Services intentionally designed to be public

If you're already SSHing into a server and just need access to one internal service, an SSH tunnel is often the fastest option.

If you need transparent access to an entire private network across multiple applications and devices, a VPN is usually the better tool.


The three commands worth remembering

If you forget everything else, remember these patterns.

Local forwarding:

ssh -L local_port:destination:port user@server
Enter fullscreen mode Exit fullscreen mode

Remote forwarding:

ssh -R remote_port:local_destination:port user@server
Enter fullscreen mode Exit fullscreen mode

Dynamic forwarding:

ssh -D local_port user@server
Enter fullscreen mode Exit fullscreen mode

-L brings something reachable from the remote side to you.

-R makes something on your side reachable from the remote side.

-D gives applications a SOCKS proxy through the remote server.

Those three patterns cover most everyday SSH tunneling use cases.


I originally published a more detailed version of this guide on the SSHFlow blog.

I'm also building SSHFlow — an SSH client where every server gets its own workspace for terminals, SFTP, code, and databases.

Top comments (0)