| |

LFCS 115 🐧 SSH Tunneling and Port Forwarding

⏱️ Estimated read: 10 min | |

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

TypeFlagSyntaxPurpose
Local-L[local:]lport:dest:dportReach remote service locally
Remote-R[remote:]rport:dest:dportExpose local service remotely
Dynamic-Dlocal-portSOCKS proxy

Common Flags

FlagPurpose
-NNo remote shell
-fBackground after auth
-CCompression
-qQuiet mode
-LLocal forwarding
-RRemote forwarding
-DDynamic forwarding

Config File Directives

DirectiveSyntax
LocalForwardLocalForward <lport> <dest>:<dport>
RemoteForwardRemoteForward <rport> <dest>:<dport>

Server Requirements

SettingPurpose
AllowTcpForwardingMust not be no
GatewayPortsyes 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

PitfallWhy It HappensFix
Local tunnel not workingAllowTcpForwarding disabledEnable on server
Remote tunnel not accessibleGatewayPorts not setSet yes or clientspecified
Tunnel closes on disconnectNo -f flagUse -f to background
Config file ignoredPermissions wrongchmod 600 ~/.ssh/config
SOCKS proxy not workingApplication not configuredSet 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

ItemValue
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 requirementAllowTcpForwarding not no
Remote bindingGatewayPorts yes or clientspecified
Config directivesLocalForward, RemoteForward

Key takeaways:

  • Local forwarding reaches remote services. The -L flag binds a local port and forwards connections to a destination reachable from the SSH server .
  • Remote forwarding exposes local services. The -R flag binds a port on the SSH server and forwards connections back to the client. GatewayPorts determines whether the port is bound to the loopback interface or all interfaces .
  • Dynamic forwarding is a SOCKS proxy. The -D flag creates a local SOCKS proxy that routes arbitrary traffic through the SSH server .
  • -N and -f are the tunnel flags. -N disables the remote shell, and -f backgrounds the process. Together, they create a silent tunnel .
  • The config file stores forwarding rules. LocalForward and RemoteForward in ~/.ssh/config make the tunnel a short alias command. The config file must be 600 .

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.

Was this helpful?


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!