SSH Tunneling — Part 1: Local Port Forwarding.
Learn how to access remote services through an encrypted SSH connection using local port forwarding (`-L`). Explore SSH commands, database access, bastion hosts, multiple tunnels, connection management, troubleshooting, and security best practices.
SSH local port forwarding creates an encrypted connection between your local machine and a remote service. It lets you access applications, databases, and internal network services without exposing their ports directly to the internet.
Common use cases include:
- Accessing remote applications bound to
localhost. - Connecting to databases without exposing database ports.
- Reaching private network services through a bastion host.
- Forwarding multiple services through a single SSH connection.
1. Understanding SSH Port Forwarding
SSH supports three types of port forwarding:
- Local forwarding (
-L): Makes a remote service accessible through a local port. - Remote forwarding (
-R): Makes a service accessible through a port on the remote side. - Dynamic forwarding (
-D): Creates a SOCKS proxy that routes connections through the SSH server.
This tutorial focuses on local port forwarding.
Understanding localhost
localhost refers to the machine on which the address is being evaluated. It usually resolves to 127.0.0.1 for IPv4.
Your Mac
localhost → Your Mac
Remote Server
localhost → Remote ServerA service listening on 127.0.0.1:3000 on a remote server is normally accessible only from that server itself.
SSH forwarding provides a secure way to access it from your computer.
Understanding bind addresses
A bind address determines which network interface accepts connections.
127.0.0.1: Accepts connections from the local machine.0.0.0.0: Listens on all IPv4 interfaces, potentially allowing other machines to connect.
For local SSH forwarding, binding to 127.0.0.1 is generally the safest default.
2. How Local Port Forwarding Works
The basic model is:
Your Machine Remote Server
localhost:8080 ── SSH ──────► localhost:3000
│
▼
Web ApplicationYour application connects to localhost:8080. SSH forwards that traffic to port 3000 on the remote server.
The destination can also be another host reachable from the SSH server, such as a database on a private network.
3. The Basic SSH Command
The general syntax is:
$ ssh -N -L [LOCAL_BIND_ADDRESS:]LOCAL_PORT:DESTINATION_HOST:DESTINATION_PORT user@REMOTE_SERVERFor example:
$ ssh -N -L 8080:localhost:3000 user@192.168.1.100Options:
-L: Enables local port forwarding.-N: Prevents execution of a remote command or opening of an interactive shell.8080: The port on your local machine.localhost:3000: The destination as seen from the remote server.
Once connected, open:
http://localhost:8080SSH carries the traffic through the encrypted connection to the remote application.
4. Accessing a Remote Web Application
Suppose a development application listens on 127.0.0.1:3000 on a remote server.
Create the tunnel:
$ ssh -N -L 127.0.0.1:8080:localhost:3000 devuser@192.168.1.100Then access the application locally at:
http://127.0.0.1:8080The application remains bound to the remote server's loopback interface. No public listener is required.
5. Accessing a Remote Database
You can use the same technique to connect to a database without exposing its port.
Suppose MySQL listens on 127.0.0.1:3306 on the remote server.
Create the tunnel:
$ ssh -N -L 127.0.0.1:3306:localhost:3306 dbadmin@staging-server.comConnect through the local port:
$ mysql -h 127.0.0.1 -P 3306 -u myappuser -pThe database connection travels through SSH. From the database server's perspective, the connection originates from the remote host running the SSH server, rather than directly from your computer.
6. Using Different Local and Remote Ports
The local and remote ports do not need to match.
For example, expose remote MySQL port 3306 locally as 13306:
$ ssh -N -L 127.0.0.1:13306:localhost:3306 user@staging-server.comConnect using:
$ mysql -h 127.0.0.1 -P 13306 -u myappuser -pThis is useful when the default local port is already occupied.
7. Forwarding Multiple Services
A single SSH connection can carry multiple local forwards.
$ ssh -N \
$ -L 127.0.0.1:3306:localhost:3306 \
$ -L 127.0.0.1:6379:localhost:6379 \
$ -L 127.0.0.1:8080:localhost:3000 \
$ user@staging-server.comThis provides access to three remote services:
localhost:3306 → Remote MySQL
localhost:6379 → Remote Redis
localhost:8080 → Remote application on port 3000All three forwards share one SSH connection.
8. Controlling Access to the Tunnel
By default, SSH local forwarding typically listens on loopback, making the forwarded port accessible only from your local machine.
Make that restriction explicit:
$ ssh -N -L 127.0.0.1:8080:localhost:3000 user@staging-server.comTo listen on all IPv4 interfaces instead:
$ ssh -N -L 0.0.0.0:8080:localhost:3000 user@staging-server.comThe second command can expose the forwarded port to other machines that can reach your computer, subject to firewall rules.
Prefer 127.0.0.1 unless other devices genuinely need access. Binding to 0.0.0.0 can unintentionally expose sensitive services.
9. Running Tunnels in the Background
Use -f to send SSH into the background after authentication:
$ ssh -f -N -L 127.0.0.1:3306:localhost:3306 user@staging-server.comTo inspect listening ports on Linux:
$ ss -lntpOn macOS:
$ lsof -nP -iTCP:3306 -sTCP:LISTENThe tunnel remains active while the SSH connection is running.
10. Keeping Tunnels Alive
SSH keepalive options help detect unresponsive connections:
$ ssh -N \
$ -o ServerAliveInterval=30 \
$ -o ServerAliveCountMax=3 \
$ -L 127.0.0.1:3306:localhost:3306 \
$ user@staging-server.comHere:
ServerAliveInterval=30sends a keepalive message every 30 seconds.ServerAliveCountMax=3allows three unanswered messages before SSH considers the connection unresponsive.
Keepalives do not automatically reconnect a failed tunnel. For automatic restart behavior, you can use autossh:
$ autossh -M 0 -N \
$ -o ServerAliveInterval=30 \
$ -o ServerAliveCountMax=3 \
$ -L 127.0.0.1:3306:localhost:3306 \
$ user@staging-server.comFor persistent production tunnels, consider managing the connection through a service manager such as systemd on Linux.
11. Forwarding Through a Bastion Host
A bastion host provides controlled access to services on a private network.
Your Machine
│
▼
Bastion Host
│
├── Database Server
├── Redis Server
└── Application ServerYou can forward several services through the bastion:
$ ssh -N \
$ -L 127.0.0.1:3306:db-server:3306 \
$ -L 127.0.0.1:6379:redis-server:6379 \
$ -L 127.0.0.1:8080:app-server:3000 \
$ user@jump-server.comThe destination hostnames are resolved from the bastion's network environment. Your computer does not need direct access to those internal services.
This approach is useful for accessing private infrastructure without exposing every service externally.
12. Troubleshooting SSH Tunnels
If a tunnel fails, start by enabling verbose SSH output:
$ ssh -vvv -N -L 127.0.0.1:8080:localhost:3000 user@staging-server.comCheck whether the local port is listening:
$ ss -lntpOn macOS:
$ lsof -nP -iTCP:8080 -sTCP:LISTENTest the local endpoint:
$ curl http://127.0.0.1:8080If the connection fails, verify that the destination service is reachable from the SSH server. For example, run this on the remote server:
$ curl http://127.0.0.1:3000Common causes include:
- The destination service is stopped.
- The destination host or port is incorrect.
- The local port is already in use.
- SSH forwarding is restricted by server configuration.
- Network or firewall rules prevent the SSH server from reaching the destination.
Remember: the destination must be reachable from the SSH server, not necessarily directly from your computer.
13. Security Best Practices
SSH tunneling encrypts traffic between the SSH endpoints, but it does not automatically make the destination application secure.
Follow these practices:
- Use SSH keys and protect private keys.
- Restrict SSH access to authorized users.
- Bind forwarded ports to
127.0.0.1unless broader access is necessary. - Avoid forwarding services you do not need.
- Audit persistent tunnels.
- Use bastion hosts to control access to private networks.
- Keep SSH clients, servers, and destination applications updated.
- Remember that traffic is not protected by the SSH tunnel after it reaches its destination.
Quick Reference
Basic local forwarding
$ ssh -N -L 8080:localhost:3000 user@serverExplicit local-only binding
$ ssh -N -L 127.0.0.1:8080:localhost:3000 user@serverDifferent local and remote ports
$ ssh -N -L 127.0.0.1:13306:localhost:3306 user@serverMultiple forwards
$ ssh -N \
$ -L 127.0.0.1:3306:localhost:3306 \
$ -L 127.0.0.1:6379:localhost:6379 \
$ user@serverBackground tunnel
$ ssh -f -N -L 127.0.0.1:3306:localhost:3306 user@serverDebugging
$ ssh -vvv -N -L 127.0.0.1:8080:localhost:3000 user@server
The Mental Model
Remember the structure:
-L LOCAL_PORT:DESTINATION_HOST:DESTINATION_PORTIt means:
Create a listening port on your machine and forward its traffic through SSH to the specified destination.
For example:
$ ssh -N -L 127.0.0.1:8080:localhost:3000 user@serverYour localhost:8080
│
│ Encrypted SSH connection
▼
Remote server's localhost:3000Once you understand this model, more advanced SSH networking becomes easier to follow.
We hope this was helpful.
Keep learning. Keep exploring. Stay sharp.
What’s Next?
In Part 2, we’ll explore remote port forwarding (-R) and how it reverses the direction of an SSH tunnel. Then, we’ll dive into dynamic port forwarding (-D) and SOCKS proxies to route traffic through an SSH server.