LFCS 115 🐧 SSH Tunneling and Port Forwarding
The previous chapters configured the SSH server and client for direct connections. This chapter covers the mechanisms that route traffic through an SSH connection to reach services that are otherwise inaccessible. SSH tunneling, also called port forwarding, creates an encrypted channel between the client and the server, and then relays traffic through that channel to a third destination. The LFCS exam tests three types: local forwarding (-L), which exposes a remote service on a local port; remote forwarding (-R), which exposes a local service on a remote port; and dynamic forwarding (-D), which creates a SOCKS proxy that routes arbitrary traffic through the SSH server . The critical prerequisite is that the SSH server must allow the forwarding type. AllowTcpForwarding must not be no, and for remote forwarding, GatewayPorts must be configured correctly .
Key point: Local forwarding (-L) is for reaching a service on the remote side from the local machine. Remote forwarding (-R) is for exposing a local service to the remote side. Dynamic forwarding (-D) is a SOCKS proxy for routing arbitrary traffic. The -N flag disables the remote shell, and the -f flag backgrounds the process. Both are commonly used together when the tunnel is the only purpose of the connection.
Why SSH tunneling exists
The inaccessible service problem. A database server is on an internal network. It is not reachable from the developer’s laptop because the firewall blocks direct access. But the developer has SSH access to a bastion host that can reach the database. Local forwarding (-L) solves this: the developer creates a tunnel from a local port to the database port, routed through the bastion. The database client connects to localhost:5432, and the traffic is encrypted and relayed through the SSH connection to the database .
The exposure problem. A developer is running a web application on localhost:3000 and wants to share it with a colleague who cannot reach the developer’s machine. Remote forwarding (-R) solves this: the developer connects to a public SSH server and asks it to listen on a port, forwarding connections back to the local application. The colleague connects to the public server’s address, and the traffic is relayed back through the tunnel . The GatewayPorts setting on the server determines whether the forwarded port is bound to localhost (only accessible from the server itself) or to all interfaces (accessible from the network).
The network restriction problem. A developer is on a network that filters traffic, and some services are unreachable. Dynamic forwarding (-D) creates a SOCKS proxy on the local machine. Any application configured to use the SOCKS proxy will route its traffic through the SSH connection, and the SSH server will make the outbound connection on the application’s behalf . This bypasses the local network’s filtering for the traffic that is proxied.
The command-line complexity problem. The full tunnel command includes the forwarding specification, the server address, the user, the key, and the flags that control the shell and the backgrounding. The .ssh/config file can store the forwarding specification with the LocalForward or RemoteForward directive, and the tunnel becomes a short alias command. The config file must be 600 or OpenSSH refuses to use it .
The trade-off. Tunneling routes traffic through the SSH server. The server’s bandwidth and CPU are consumed by the relayed traffic. The tunnel is only as secure as the SSH connection, and the SSH connection is only as secure as the key management. A tunnel that is left open indefinitely is a security risk; the -f flag backgrounds the process, but it does not add an automatic termination.
a. Local port forwarding (-L)
Local forwarding binds a port on the local machine and forwards connections to a destination reachable from the SSH server. The syntax is ssh -L [local-ip:]local-port:destination-ip:destination-port [user@]ssh-server.
# Forward local port 5432 to the database's port 5432 through the bastion
ssh -L 5432:db.internal.example.com:5432 user@bastion.example.com
# Forward local port 8080 to a web server's port 80
ssh -L 8080:web.internal.example.com:80 user@bastion.example.com
The local port is bound to localhost unless a local IP is specified. The destination is resolved by the SSH server, not by the local machine. This is the key mechanism: the server has network access to the destination, and the client does not .
The -N flag disables the remote command execution. Without it, the SSH session opens a shell on the server, which is unnecessary when the tunnel is the only purpose. The -f flag backgrounds the process after authentication. Together, -N -f creates a tunnel that runs silently in the background .
# Background tunnel, no shell
ssh -N -f -L 5432:db.internal.example.com:5432 user@bastion.example.com
The -L flag can be used multiple times in the same command to forward multiple ports. Each -L creates a separate forwarding rule.
b. Remote port forwarding (-R)
Remote forwarding binds a port on the SSH server and forwards connections back to a destination reachable from the SSH client. The syntax is ssh -R [remote-ip:]remote-port:destination-ip:destination-port [user@]ssh-server.
# Expose local port 3000 on the remote server's port 8080
ssh -R 8080:localhost:3000 user@public.example.com
The remote server listens on port 8080. A connection to public.example.com:8080 is forwarded through the tunnel to localhost:3000 on the client machine . The localhost in the forwarding specification is resolved by the SSH client, not by the server. This is the opposite of local forwarding.
By default, the remote port is bound to the loopback interface on the server. Only users on the server itself can connect to it. To bind to all interfaces, the server’s sshd_config must have GatewayPorts yes or GatewayPorts clientspecified. The clientspecified value allows the client to specify the bind address in the -R argument .
# Bind to all interfaces (requires GatewayPorts on the server)
ssh -R 0.0.0.0:8080:localhost:3000 user@public.example.com
c. Dynamic port forwarding (-D) and the config file
Dynamic forwarding creates a SOCKS proxy on the local machine. The SSH server acts as the exit point for all connections that are routed through the proxy. The syntax is ssh -D local-port [user@]ssh-server.
# SOCKS proxy on local port 1080
ssh -D 1080 -N -f user@ssh-server.example.com
Any application that supports SOCKS5 can be configured to use localhost:1080. The application’s traffic is sent to the proxy, which forwards it through the SSH connection, and the SSH server makes the outbound connection . The -C flag enables compression, which is useful on slow connections. The -q flag suppresses the warning messages .
For repeated use, the forwarding specification can be stored in ~/.ssh/config. The LocalForward and RemoteForward directives take the local port and the destination as separate arguments, separated by a space, not a colon.
# ~/.ssh/config
Host db-tunnel
HostName bastion.example.com
User alice
LocalForward 5432 db.internal.example.com:5432
Host preview
HostName public.example.com
User alice
RemoteForward 8080 localhost:3000
The tunnel is started with ssh -N db-tunnel or ssh -N preview. The -N flag is still required to disable the shell, because the config file does not store that flag . The config file must be 600 or OpenSSH refuses to read it.
Complete Example Session
# ============================================
# PART 1: LOCAL FORWARDING
# ============================================
ssh -L 5432:db.internal.example.com:5432 user@bastion.example.com
# A shell opens on the bastion.
# The database is reachable at localhost:5432 on the client.
# ============================================
# PART 2: BACKGROUND LOCAL FORWARDING
# ============================================
ssh -N -f -L 5432:db.internal.example.com:5432 user@bastion.example.com
# No shell. The tunnel runs in the background.
# ============================================
# PART 3: TEST THE LOCAL TUNNEL
# ============================================
psql -h localhost -p 5432 -U dbuser mydb
# Connects through the tunnel to the internal database.
# ============================================
# PART 4: REMOTE FORWARDING
# ============================================
ssh -R 8080:localhost:3000 user@public.example.com
# The public server listens on port 8080.
# Connections to public.example.com:8080 reach localhost:3000.
# ============================================
# PART 5: REMOTE FORWARDING WITH GATEWAYPORTS
# ============================================
ssh -R 0.0.0.0:8080:localhost:3000 user@public.example.com
# Binds to all interfaces (requires GatewayPorts on the server).
# ============================================
# PART 6: DYNAMIC FORWARDING
# ============================================
ssh -D 1080 -N -f user@ssh-server.example.com
# SOCKS proxy on localhost:1080.
# ============================================
# PART 7: CONFIGURE THE PROXY
# ============================================
# In Firefox: Settings → Network Settings → Manual proxy
# SOCKS Host: localhost, Port: 1080, SOCKS v5
# ============================================
# PART 8: CONFIG FILE FOR LOCAL FORWARDING
# ============================================
# ~/.ssh/config
Host db-tunnel
HostName bastion.example.com
User alice
LocalForward 5432 db.internal.example.com:5432
ssh -N db-tunnel
# ============================================
# PART 9: VERIFY THE TUNNEL
# ============================================
ss -tlnp | grep 5432
# LISTEN 0 128 127.0.0.1:5432
The nine parts trace the tunneling workflow: local forwarding, backgrounding, testing, remote forwarding, remote with GatewayPorts, dynamic forwarding, proxy configuration, config file, and verification.
Quick Reference
Forwarding Types
| Type | Flag | Syntax | Purpose |
|---|---|---|---|
| Local | -L | [local:]lport:dest:dport | Reach remote service locally |
| Remote | -R | [remote:]rport:dest:dport | Expose local service remotely |
| Dynamic | -D | local-port | SOCKS proxy |
Common Flags
| Flag | Purpose |
|---|---|
-N | No remote shell |
-f | Background after auth |
-C | Compression |
-q | Quiet mode |
-L | Local forwarding |
-R | Remote forwarding |
-D | Dynamic forwarding |
Config File Directives
| Directive | Syntax |
|---|---|
LocalForward | LocalForward <lport> <dest>:<dport> |
RemoteForward | RemoteForward <rport> <dest>:<dport> |
Server Requirements
| Setting | Purpose |
|---|---|
AllowTcpForwarding | Must not be no |
GatewayPorts | yes or clientspecified for remote binding |
Best Practices
✅ Do This:
ssh -N -f -L 5432:db.internal:5432 user@bastion
<!-- Background local tunnel --> # ✅
Host db-tunnel
LocalForward 5432 db.internal:5432
<!-- Store forwarding in config --> # ✅
ssh -D 1080 -C -N -f user@proxy
<!-- Dynamic forwarding with compression --> # ✅
❌ Don’t Do This:
# Leave the tunnel open indefinitely
<!-- Kill the process when done --> # ❌
# Assume GatewayPorts is enabled by default
<!-- Check the server config for remote forwarding --> # ❌
# Forget -N for a tunnel-only connection
<!-- A shell opens unnecessarily --> # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Local tunnel not working | AllowTcpForwarding disabled | Enable on server |
| Remote tunnel not accessible | GatewayPorts not set | Set yes or clientspecified |
| Tunnel closes on disconnect | No -f flag | Use -f to background |
| Config file ignored | Permissions wrong | chmod 600 ~/.ssh/config |
| SOCKS proxy not working | Application not configured | Set SOCKS5 proxy to localhost:port |
Real-World Examples
1. Local Forwarding
ssh -L 5432:db.internal:5432 user@bastion
2. Background Local
ssh -N -f -L 5432:db.internal:5432 user@bastion
3. Remote Forwarding
ssh -R 8080:localhost:3000 user@public
4. Remote with GatewayPorts
ssh -R 0.0.0.0:8080:localhost:3000 user@public
5. Dynamic Forwarding
ssh -D 1080 -N -f user@proxy
6. Config File
Host db-tunnel
LocalForward 5432 db.internal:5432
7. Verify Tunnel
ss -tlnp | grep 5432
8. Kill Tunnel
ps -ef | grep "ssh -f"
kill <pid>
9. Multiple Forwards
ssh -L 5432:db:5432 -L 6379:redis:6379 user@bastion
10. SOCKS with Firefox
Settings → Network → Manual → SOCKS localhost:1080
Visual
Local Forwarding
┌──────────────────────────────────────────────┐
│ Client │
│ localhost:5432 │
└──────────────────┬───────────────────────────┘
│
│ SSH tunnel
▼
┌──────────────────────────────────────────────┐
│ SSH Server (bastion) │
└──────────────────┬───────────────────────────┘
│
│ forwarded connection
▼
┌──────────────────────────────────────────────┐
│ Destination (db.internal:5432) │
└──────────────────────────────────────────────┘
Remote Forwarding
┌──────────────────────────────────────────────┐
│ Client │
│ localhost:3000 │
└──────────────────┬───────────────────────────┘
│
│ SSH tunnel
▼
┌──────────────────────────────────────────────┐
│ SSH Server (public) │
│ Listens on :8080 │
└──────────────────┬───────────────────────────┘
│
│ forwarded connection
▼
┌──────────────────────────────────────────────┐
│ External user │
│ connects to public:8080 │
└──────────────────────────────────────────────┘
Dynamic Forwarding
┌──────────────────────────────────────────────┐
│ Application (browser, curl) │
│ SOCKS5 proxy → localhost:1080 │
└──────────────────┬───────────────────────────┘
│
│ SSH tunnel
▼
┌──────────────────────────────────────────────┐
│ SSH Server │
│ Makes outbound connections │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Arbitrary destinations │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Local forwarding | -L [local:]lport:dest:dport |
| Remote forwarding | -R [remote:]rport:dest:dport |
| Dynamic forwarding | -D local-port |
| No shell | -N |
| Background | -f |
| Compression | -C |
| Server requirement | AllowTcpForwarding not no |
| Remote binding | GatewayPorts yes or clientspecified |
| Config directives | LocalForward, RemoteForward |
Key takeaways:
- Local forwarding reaches remote services. The
-Lflag binds a local port and forwards connections to a destination reachable from the SSH server . - Remote forwarding exposes local services. The
-Rflag binds a port on the SSH server and forwards connections back to the client.GatewayPortsdetermines whether the port is bound to the loopback interface or all interfaces . - Dynamic forwarding is a SOCKS proxy. The
-Dflag creates a local SOCKS proxy that routes arbitrary traffic through the SSH server . -Nand-fare the tunnel flags.-Ndisables the remote shell, and-fbackgrounds the process. Together, they create a silent tunnel .- The config file stores forwarding rules.
LocalForwardandRemoteForwardin~/.ssh/configmake the tunnel a short alias command. The config file must be600.
Remember: SSH tunneling is the mechanism that extends the reach of the SSH connection beyond a shell. Local forwarding brings a remote service to the local machine. Remote forwarding exposes a local service to the remote side. Dynamic forwarding turns the SSH connection into a SOCKS proxy. The -N flag removes the shell, the -f flag backgrounds the process, and the config file stores the rule. The server must allow the forwarding, and the client must have the key.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!