Skip to content
DevOps devops networking 5 min read

SSH Tunneling & Port Forwarding

SSH tunneling is a way to send the traffic of another program through an encrypted SSH connection. Instead of using SSH only to get a shell on a remote machine, you use it as a secure pipe between two ports. This lets you reach services that are locked away on a remote server (like a database that only listens on localhost) without exposing them to the public internet. It is one of the most useful and most misunderstood tools in a DevOps engineer’s kit.

A tunnel here just means: “take a port on one machine, and make it behave as if it were a port on another machine.” A port is a numbered door on a server where a specific service listens (for example, PostgreSQL listens on port 5432). SSH can carry the traffic for that door across the encrypted connection.

Why tunnels matter

Many important services are deliberately bound to localhost (also written 127.0.0.1), meaning they only accept connections coming from the same machine. A database, an admin dashboard, or a metrics endpoint is often configured this way on purpose so the outside world cannot touch it. The problem: you are on the outside world too.

A tunnel solves this. You already have a trusted, encrypted SSH login to the server. SSH can forward a port through that same login, so the locked-down service becomes reachable from your laptop, while staying invisible to everyone else.

There are two main directions:

TypeFlagDirectionUse it when
Local forwarding-LYour machine -> remote serviceYou want to reach a service that lives on (or near) the remote server
Remote forwarding-RRemote machine -> your local serviceYou want the remote server to reach a service running on your laptop

Local forwarding (-L)

Local forwarding opens a port on your machine. Anything that connects to that local port is sent through SSH and out to a destination chosen on the remote side.

The syntax is:

ssh -L LOCAL_PORT:DESTINATION_HOST:DESTINATION_PORT user@server

Read it as: “On my machine, open LOCAL_PORT. Forward its traffic through server, then on to DESTINATION_HOST:DESTINATION_PORT.”

Example: reach a remote database bound to localhost

Say your Ubuntu server runs PostgreSQL, and /etc/postgresql/16/main/postgresql.conf has listen_addresses = 'localhost'. The database is on port 5432 but only accepts local connections. You want to query it from your laptop with a GUI tool.

ssh -L 5432:localhost:5432 [email protected]

While that SSH session stays open, your laptop’s port 5432 is wired straight to the server’s PostgreSQL. Point your database client at localhost:5432 and you are talking to the remote database over an encrypted channel.

The localhost in 5432:localhost:5432 is evaluated on the remote server, not on your laptop. That is why the database does not need to be exposed publicly — SSH connects to it locally, from the server’s own point of view.

Run a tunnel in the background

If you only want the tunnel and not an interactive shell, add -f (go to background) and -N (do not run a remote command):

ssh -fNL 5432:localhost:5432 [email protected]

Output:

It prints nothing and returns you to your prompt. The tunnel is now running quietly. To find and stop it later:

pgrep -af "ssh -fNL 5432"

Output:

48213 ssh -fNL 5432:localhost:5432 [email protected]
kill 48213

When to use local forwarding (and when not)

  • Use it to reach internal services: databases, Redis, a private admin UI, a service running only on a server inside a private network the SSH host can see.
  • Do not use it as a permanent production link. Tunnels die when the SSH session drops. For long-lived connections use a proper VPN or a managed connection. For resilient tunnels, tools like autossh can restart them automatically.

Remote forwarding (-R)

Remote forwarding is the mirror image. It opens a port on the remote server that forwards back to a service on your machine. This is how you let a server (or someone on the server) reach something running on your laptop.

ssh -R REMOTE_PORT:DESTINATION_HOST:DESTINATION_PORT user@server

Example: expose your local dev app to a remote server

You are building a web app on your laptop at localhost:3000, and you want the remote server to be able to reach it (for a webhook test, say).

ssh -R 8080:localhost:3000 [email protected]

Now anything on the server that connects to localhost:8080 is forwarded back to your laptop’s port 3000.

By default the forwarded remote port binds only to localhost on the server, so only the server itself can use it. To let other machines reach it, the server’s /etc/ssh/sshd_config must set GatewayPorts yes and you bind explicitly:

ssh -R 0.0.0.0:8080:localhost:3000 [email protected]

Opening a remote port to 0.0.0.0 (all interfaces) exposes your local service to anyone who can reach that server’s port. Only do this with ufw rules in place and never with sensitive apps. This is exactly how attackers create backdoors — treat -R to 0.0.0.0 as a loaded weapon.

Keeping tunnels alive

Idle tunnels can be dropped by firewalls. Add keepalives so SSH sends a quiet ping periodically. You can set this per command or globally in ~/.ssh/config:

Host db-tunnel
    HostName 203.0.113.10
    User deploy
    LocalForward 5432 localhost:5432
    ServerAliveInterval 30
    ServerAliveCountMax 3

Now ssh -fN db-tunnel starts the same tunnel with a friendly name.

Best Practices

  • Prefer SSH key authentication over passwords for any tunnel you script or automate.
  • Keep forwarded ports bound to localhost unless you have a deliberate, firewalled reason to expose them.
  • Use ~/.ssh/config entries so tunnel definitions are documented, repeatable, and easy for teammates to read.
  • Add ServerAliveInterval to survive idle timeouts on long-running tunnels.
  • For production-grade, self-healing tunnels, use autossh (sudo apt install autossh) instead of a bare ssh -f.
  • Audit GatewayPorts and AllowTcpForwarding in sshd_config on your servers — disable forwarding entirely on hosts that should never tunnel.
  • Always close tunnels when finished; a forgotten -R tunnel is a standing security hole.
Last updated June 15, 2026
Was this helpful?