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.
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 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 to127.0.0.1on the server unlessGatewayPorts yesis set. - DNS resolved on the wrong side: with
-L, the destination is resolved on the bastion.localhostmeans the bastion’s loopback, not yours. - Dropped tunnels: NAT timeouts kill idle tunnels. Use
ServerAliveIntervalso 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
-Nwhen you only want the tunnel, not a shell. - Combine
-fwith-Nto background the tunnel after authentication. - Set
ExitOnForwardFailure yesso the SSH process dies if the forward cannot bind, instead of silently leaving you without a tunnel. - For repeated workflows, put forwards in
~/.ssh/configso coworkers can copy your setup. - Prefer
autosshfor 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.
Related articles
- Linux Linux iptables Firewall Basics
Learn iptables tables, chains, and rules. Build a working host firewall, understand NAT, and avoid the most common mistakes that lock you out.
- Linux Linux Network Namespaces Tutorial
Hands-on guide to Linux network namespaces. Create isolated network stacks, connect them with veth pairs, and understand how containers get networking.
- Linux Linux Networking with ip and ss: The Modern Toolkit
Replace ifconfig and netstat with ip and ss. Learn to inspect interfaces, routes, and sockets on modern Linux with clear examples.
- Linux Linux Networking Commands: ip, ss, curl, dig
A practical guide to modern Linux networking tools. Learn ip, ss, curl, and dig to diagnose connectivity, inspect sockets, and resolve DNS issues fast.