SSH tunneling in Linux is one of those skills that separates casual users from real sysadmins. Once you understand how to forward ports through an encrypted SSH connection, you unlock a whole new level of control over your network. No more exposing database ports to the internet. No more sketchy VPN setups just to reach a service behind a firewall.
I remember the exact moment SSH tunneling clicked for me. I was sitting in a coffee shop, trying to access a PostgreSQL database running on my homelab. The port wasn’t exposed publicly (obviously), and I didn’t want to mess with my router’s port forwarding. Then a friend on IRC said two words: “SSH tunnel.” Five minutes later, I was connected. That moment changed how I think about networking entirely.
In this guide, I’ll walk you through every type of SSH port forwarding: local, remote, and dynamic. You’ll also learn about jump hosts, persistent tunnels with autossh, and config file shortcuts that save you from retyping long commands. Whether you’re accessing cloud databases, sharing a local dev server, or routing traffic through a SOCKS proxy, this guide has you covered.
What Is SSH Tunneling (And Why Every Sysadmin Needs It)
SSH tunneling (also called SSH port forwarding) wraps your TCP traffic inside an encrypted SSH connection. Instead of sending data directly between two points, you route it through an SSH session. The traffic enters one end of the tunnel encrypted, travels across the network, and exits at the other end.
“Every developer, sysadmin, and DevOps engineer uses SSH daily, yet most barely scratch the surface of what it can do.” — Teleport Engineering Team
![]()
Get a VPS from as low as $11/year! WOW!
Think of it like this: you have a garden hose (your data), and you run it through a locked, armored pipe (the SSH connection). Anyone watching the pipe can’t see what’s flowing inside. That’s the core idea.
There are three types of SSH tunneling you need to know:
- Local forwarding (-L): Brings a remote service to your local machine
- Remote forwarding (-R): Exposes your local service through a remote server
- Dynamic forwarding (-D): Creates a flexible SOCKS proxy for routing any traffic
SSH tunneling solves a fundamental problem: how do you access services on a private network without punching holes in your firewall? The answer is you don’t punch holes at all. You tunnel through an existing SSH connection. If you’re already using SCP for secure file transfers or rsync over SSH, you’re already using the same underlying protocol.
How SSH Tunneling Actually Works
Under the hood, SSH tunneling uses channel multiplexing as defined in RFC 4254: SSH Connection Protocol. A single SSH connection can carry multiple channels. Each tunnel you create opens a new channel within that connection.
When you set up a local forward, your SSH client listens on a local port. Any connection to that port gets forwarded through the SSH session to the destination you specified. The remote SSH server then connects to the target host and port on your behalf. All of this happens over the encrypted SSH connection you already have open.
Prerequisites Before You Start
Before diving into tunnel commands, make sure you have these basics in place:
- SSH access to a remote server: You need a machine you can SSH into. Key-based authentication is strongly recommended. If you haven’t set that up yet, follow my guide to generate an SSH key pair.
- OpenSSH installed: Both your local machine and the remote server need the OpenSSH official project client and server packages. Most Linux distros ship with OpenSSH 8.x or 9.x by default.
- Basic understanding of ports: Know what a port number is and that services bind to specific ports (e.g., PostgreSQL on 5432, HTTP on 80).
You can verify your OpenSSH version with:
ssh -V
If you see version 7.3 or higher, you have access to all features covered in this guide, including ProxyJump.
Local Port Forwarding (-L): Access Remote Services on Your Machine
Local port forwarding is the most common type of SSH tunnel. It lets you access a service running on a remote network as if it were running on your own machine.
The -L Flag: Syntax Explained
The basic syntax looks like this:
ssh -L [local_port]:[remote_host]:[remote_port] user@ssh-server
Here’s what each piece means:
- local_port: The port on your machine where you’ll connect
- remote_host: The target host as seen from the SSH server (often
localhost) - remote_port: The port of the service you want to reach
- user@ssh-server: Your SSH login to the intermediary server
Real-World Example: Securely Access a Remote Database
Let’s say you have a PostgreSQL database running on a server, but port 5432 is only listening on localhost (as it should be). You want to connect from your laptop:
ssh -L 5432:localhost:5432 [email protected]
Now open another terminal and connect to PostgreSQL as if it were local:
psql -h 127.0.0.1 -p 5432 -U myuser mydatabase
The traffic flows: your laptop → SSH tunnel → db-server → PostgreSQL on localhost:5432. The database port never touches the public internet.
💡 Pro Tip
If port 5432 is already in use locally, just pick a different local port: ssh -L 15432:localhost:5432 alexa@db-server. Then connect to 127.0.0.1:15432 instead.
Another common use case: accessing a web admin panel that’s only available on the server’s localhost:
ssh -L 8080:localhost:80 [email protected]
Then open http://localhost:8080 in your browser. Simple and secure.
Useful Flags: -N, -f, and -C
These flags make your tunnels more practical:
- -N: Don’t execute any remote commands. Perfect when you only need the tunnel, not a shell session.
- -f: Fork the SSH process into the background after connecting. Combine with
-Nfor a background tunnel. - -C: Enable compression. Helpful over slow connections.
Here’s the command I use most often in practice:
ssh -fNL 5432:localhost:5432 [email protected]
That creates a background tunnel with no shell. Clean and efficient.
Remote Port Forwarding (-R): Expose Your Local Service Through a Server
Remote port forwarding works in the opposite direction. Instead of pulling a remote service to your machine, you push a local service out through a remote server.
The -R Flag: Syntax Explained
ssh -R [remote_port]:[local_host]:[local_port] user@ssh-server
This tells the SSH server to listen on remote_port and forward connections back to your local machine on local_port.
Real-World Example: Share a Local Dev Server
Imagine you’re running a web app locally on port 3000, and a client needs to preview it. You have a public server available:
ssh -R 8080:localhost:3000 [email protected]
Now anyone who visits public-server.example.com:8080 will see your local dev server. No deployment needed. I’ve used this trick more times than I can count during freelance projects. It beats setting up a staging environment when you just need quick feedback.
Enabling GatewayPorts in sshd_config
By default, remote forwarded ports only bind to the server’s localhost (127.0.0.1). If you want external users to reach the forwarded port, you need to edit the SSH server config:
sudo nano /etc/ssh/sshd_config
Add or change this line:
GatewayPorts yes
Then restart SSH:
sudo systemctl restart sshd
⚠️ Security Warning
Enabling GatewayPorts yes allows anyone on the network to connect to your forwarded port. Only do this on servers protected by a firewall. Consider using configure UFW firewall rules or iptables firewall rules to restrict access.
Dynamic Port Forwarding (-D): Create a SOCKS Proxy
Dynamic forwarding is the most flexible option. Instead of mapping one specific port, it creates a SOCKS proxy protocol proxy that can route traffic to any destination.
The -D Flag: Syntax Explained
ssh -D [local_port] user@ssh-server
This opens a SOCKS5 proxy on your local machine. Any application that supports SOCKS can route its traffic through the tunnel.
ssh -D 1080 [email protected]
Now you have a SOCKS5 proxy listening on localhost:1080.
Configuring Firefox or curl to Use the SOCKS Proxy
To route Firefox traffic through your tunnel, go to Settings → Network Settings → Manual Proxy Configuration. Set SOCKS Host to 127.0.0.1, port 1080, and select SOCKS v5.
For command-line testing with curl:
curl --socks5 localhost:1080 https://ifconfig.me
If the returned IP matches your SSH server instead of your local IP, the tunnel is working. I use this setup whenever I’m on hotel WiFi or any network I don’t trust. It’s not a full VPN, but it encrypts your browsing traffic through a server you control.
SSH Jump Hosts with ProxyJump (-J): Hop Through Bastion Servers
In cloud environments, you often can’t reach internal servers directly. You first connect to a bastion (jump) host, then hop to your target. ProxyJump makes this painless.
Using the -J Flag
ssh -J bastion.example.com alexa@internal-server
This connects to bastion.example.com first, then automatically hops to internal-server. The connection is end-to-end encrypted — the bastion host can’t see your traffic.
You can even chain multiple jump hosts:
ssh -J jump1.example.com,jump2.example.com alexa@target-server
ProxyJump was introduced in OpenSSH 7.3 as a cleaner replacement for the older ProxyCommand directive. If you’re accessing AWS EC2 instances through a bastion, this is the modern way to do it.
Setting Up ProxyJump in ~/.ssh/config
For hosts you jump to regularly, add it to your SSH config:
Host bastion
HostName bastion.example.com
User alexa
IdentityFile ~/.ssh/id_ed25519
Host internal-db
HostName 10.0.1.50
User alexa
ProxyJump bastion
Now just run ssh internal-db, and SSH handles the rest. No more remembering complex flags.
Keeping Tunnels Alive with autossh
SSH tunnels die. Networks hiccup, connections time out, and suddenly your tunnel is gone. I learned this the hard way when my homelab database tunnel dropped during a long-running migration at 2 AM. Enter autossh.
autossh monitors your SSH connection and restarts it automatically if it drops. Install it with your package manager:
sudo apt install autossh # Debian/Ubuntu
sudo pacman -S autossh # Arch, btw
Here’s the command I run for persistent tunnels:
autossh -M 0 -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" -fN -L 5432:localhost:5432 alexa@db-server
The -M 0 flag disables autossh’s own monitoring port and relies on SSH’s built-in keepalive instead. This is the recommended approach for modern setups.
For tunnels that need to survive reboots, combine autossh with a systemd service. You can follow my guide on how to create a systemd service to set that up. Alternatively, if you just need a tunnel alive during an interactive session, you can keep terminal sessions running with tmux.
Save Time with SSH Config File Shortcuts
If you’re typing out full tunnel commands every time, you’re working too hard. The ~/.ssh/config file lets you define named hosts with tunnel settings baked in.
Here’s an example config block for a database tunnel:
Host db-tunnel
HostName db-server.example.com
User alexa
IdentityFile ~/.ssh/id_ed25519
LocalForward 5432 localhost:5432
ServerAliveInterval 60
ServerAliveCountMax 3
Now instead of the full command, just run:
ssh -fN db-tunnel
You can use LocalForward, RemoteForward, and DynamicForward directives. Stack multiple forwards in a single host block:
Host work-tunnels
HostName work-server.example.com
User alexa
LocalForward 5432 localhost:5432
LocalForward 6379 localhost:6379
DynamicForward 1080
One command, three tunnels. This is the kind of config that makes your workflow feel effortless.
Common SSH Tunnel Errors and Fixes
Even experienced sysadmins hit these errors. Here’s how to fix the most common ones fast.
Bind: Address Already in Use
This means something is already listening on the local port you’re trying to forward. Use lsof to find what’s using a port:
lsof -ti:5432 | xargs kill
Or use the ss command to check open ports before starting your tunnel:
ss -tlnp | grep 5432
You can also check with the netstat command or check listening ports for a broader view of what’s running.
Channel X: Open Failed: Connect Failed
This error means the SSH server successfully received your forwarded connection, but it couldn’t reach the target host or port. Common causes:
- The target service isn’t running on the remote host
- You specified the wrong host or port
- A firewall on the remote server is blocking the connection
Verify the port is open from the SSH server itself. You can scan ports with nmap or use resolve hostnames with dig to verify DNS is resolving correctly. For a broader set of diagnostics, check out my guide on how to troubleshoot network issues in Linux.
Tunnel Drops After Idle
SSH connections that sit idle often get killed by firewalls or NAT devices. The fix is to send keepalive packets. Add these to your ~/.ssh/config:
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
This sends a keepalive every 60 seconds and disconnects after 3 missed responses. For truly persistent tunnels, use autossh as described above.
Security Best Practices for SSH Tunnels
SSH tunnels are powerful, but they can also be a security risk if misconfigured. Here’s how to keep things locked down.
- Always use key-based authentication: Never rely on passwords for SSH. Keys are stronger and can be rotated. If you haven’t switched yet, generate an SSH key pair today.
- Bind to 127.0.0.1, not 0.0.0.0: By default, SSH tunnels bind to localhost. Don’t change this unless you have a specific reason and a firewall in place.
- Restrict forwarding per user: In
/etc/ssh/sshd_config, you can disable forwarding for specific users:Match User deployfollowed byAllowTcpForwarding no. - Use fail2ban: Protect SSH with fail2ban to block brute-force attacks against your SSH server.
- Audit and log: Monitor who’s creating tunnels. In enterprise environments, consider short-lived SSH certificates instead of long-lived keys.
“SSH tunnels solve the security problem of having to expose ports for services to the internet, offering a more viable alternative than physical perimeters or VPNs in the context of remote work and cloud infrastructure.” — Alessio Ligabue, Linux Security Engineer
Start Tunneling Smarter
SSH tunneling in Linux isn’t just a neat trick — it’s a fundamental skill for anyone managing servers, working with cloud infrastructure, or caring about security. Once you get comfortable with -L, -R, and -D, you’ll find yourself reaching for SSH tunnels constantly.
My advice? Start with local forwarding for a database connection. Get that working. Then experiment with the SSH config file shortcuts so you never type the full command again. Once that clicks, explore dynamic forwarding and jump hosts. Build the muscle memory and it becomes second nature.
If you’re looking to level up your Linux networking skills further, check out my guides on how to configure UFW firewall for server hardening, or learn how to SCP for secure file transfers over those same SSH connections. And if you ever run into connectivity issues, my troubleshoot network issues in Linux guide walks through every diagnostic tool you need.





