2026年11月12日
2026年11月12日
SSH Local Port Forwarding (ssh -L): Reach Private Services Safely
This article is a localized edition of the Japanese original. Read the Japanese original
SSH local port forwarding (ssh -L) takes connections that arrive at a port on your own PC and delivers them through the SSH connection to a destination on the server side. Through a server you can log in to over SSH, you can reach a MySQL server that is not exposed to the internet, or an admin page inside a private network.
The commands in this article were tested with OpenSSH 9.6 (as of October 2026). The OpenSSH client in Windows 10 (version 1809 or later) and Windows 11 accepts the same syntax from PowerShell.
Syntax and how to read it
ssh -L [listen_address:]listen_port:destination_host:destination_port user@ssh_server
| Part | Meaning |
|---|---|
| listen_address | The address to listen on, on your PC. If omitted, it listens only on 127.0.0.1 (and ::1) |
| listen_port | The port number to listen on, on your PC |
| destination_host:destination_port | The destination as seen from the SSH server. 127.0.0.1 means the SSH server itself |
| user@ssh_server | The server you log in to over SSH |
The destination host is where people go wrong. The SSH server, not your PC, resolves the name and connects to it, so 127.0.0.1 means "the SSH server itself" and 10.0.0.20 means "a host on the internal network that the SSH server can reach."
Only the leg between your PC and the SSH server is encrypted. From the SSH server to the destination host the traffic is ordinary, so if the destination is a different host, make sure the network between them can be trusted.
Example 1: Connect to the server’s MySQL from your PC
If MySQL on the database server listens only on 127.0.0.1:3306 and cannot be reached from outside, forward local port 3307 to it.
ssh -N -L 3307:127.0.0.1:3306 user@db.example.com
-N tells ssh not to run a remote command (forwarding only). Leave this terminal open and connect from another terminal.
mysql -h 127.0.0.1 -P 3307 -u wpuser -p
If you write -h localhost, the MySQL client tries to connect through a socket file, so write 127.0.0.1 when you use a tunnel. MySQL often already uses port 3306 on your own PC as well, so the example listens on a different port (3307).
Example 2: Open an internal admin page through a bastion host
To open, in your local browser, an admin page served on port 8080 of the internal server 10.0.0.20, which is reachable only from the bastion host (bastion.example.com):
ssh -N -L 8080:10.0.0.20:8080 user@bastion.example.com
Open http://127.0.0.1:8080/ in your browser.
If the destination uses HTTPS and routes by host name (name-based virtual hosts), opening https://127.0.0.1:8443/ gives a certificate name mismatch warning or shows a different site. In that case, open it by host name pointed at 127.0.0.1. With curl, you can do this with --resolve.
ssh -N -L 8443:10.0.0.20:443 user@bastion.example.com
# In another terminal (treat intranet.example.com as 127.0.0.1)
curl --resolve intranet.example.com:8443:127.0.0.1 https://intranet.example.com:8443/
In a browser, map intranet.example.com to 127.0.0.1 in your local hosts file and open https://intranet.example.com:8443/. Remove the line from the hosts file when you are done.
Forward several ports at once
You can give -L as many times as you like in one connection.
ssh -N -L 3307:127.0.0.1:3306 -L 6380:127.0.0.1:6379 user@db.example.com
Run in the background without missing forwarding failures
If you do not want the tunnel to occupy a terminal, add -f: ssh moves to the background after authentication. Specify ExitOnForwardFailure=yes together with it.
ssh -f -N -o ExitOnForwardFailure=yes -L 3307:127.0.0.1:3306 user@db.example.com
Without ExitOnForwardFailure, if the listen port is already in use, ssh only prints the following warning and keeps the connection open. The connection stays up even though nothing is forwarded, which is easy to miss. With the option, ssh exits when it cannot set up the forwarding (exit code 255).
bind [127.0.0.1]:3307: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 3307
Could not request local forwarding.
To check that the port is listening, run this on your PC (Linux):
ss -ltnp | grep 3307
Common errors
| Message | Cause |
|---|---|
bind [127.0.0.1]:3307: Address already in use |
Another program or an earlier ssh is using the local listen port |
channel 2: open failed: connect failed: Connection refused |
The SSH server cannot connect to the destination host and port (the service is stopped, the port is wrong, and so on) |
channel 2: open failed: administratively prohibited: open failed |
The SSH server configuration (AllowTcpForwarding, PermitOpen) forbids the forwarding |
The last two appear in the terminal running ssh when you connect to the forwarded port. They are hard to see when ssh runs in the background with -f, so drop -f while troubleshooting.
Put it in ~/.ssh/config
If you use the same forwarding regularly, write it in ~/.ssh/config so a short command is enough.
Host db-tunnel
HostName db.example.com
User user
LocalForward 127.0.0.1:3307 127.0.0.1:3306
ExitOnForwardFailure yes
ServerAliveInterval 30
ServerAliveCountMax 3
ssh -f -N db-tunnel
ServerAliveInterval 30 and ServerAliveCountMax 3 send a keepalive every 30 seconds and drop the connection after three unanswered ones. This prevents ssh from lingering after the link has silently died.
Stop the forwarding
In the foreground, Ctrl+C stops it. If you sent it to the background with -f, find the process and stop it.
pgrep -af 'ssh .*-L 3307'
kill PID
If you plan to stop it later, creating a control socket lets you stop it without looking up the process ID.
# -M makes this a control master connection, -S sets the socket path
ssh -f -N -M -S ~/.ssh/tunnel-db.sock -L 3307:127.0.0.1:3306 user@db.example.com
# Check the status and stop it
ssh -S ~/.ssh/tunnel-db.sock -O check user@db.example.com
ssh -S ~/.ssh/tunnel-db.sock -O exit user@db.example.com
Reconnect automatically when it drops (keeping it running)
Do not repeatedly start ssh in the background in a loop such as while true; do ssh -L ... & sleep 1200; done. The next ssh starts while the previous one is still running, so you accumulate ssh processes that fail to listen on the same port and unneeded connections. It also cannot detect that a connection has dropped.
Leave reconnection to something that restarts ssh when it exits. With either method below, make sure you can log in without typing a password, using a key without a passphrase or ssh-agent (connect once manually first to register the host key in known_hosts).
Run it as a systemd service (Linux)
Create /etc/systemd/system/ssh-tunnel-db.service. Replace deploy with the user that opens the tunnel (the user who owns the key and known_hosts).
[Unit]
Description=SSH tunnel 127.0.0.1:3307 to db.example.com:3306
After=network-online.target
Wants=network-online.target
[Service]
User=deploy
ExecStart=/usr/bin/ssh -N -o BatchMode=yes -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -L 127.0.0.1:3307:127.0.0.1:3306 user@db.example.com
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-tunnel-db.service
systemctl status ssh-tunnel-db.service
journalctl -u ssh-tunnel-db.service -n 20
Do not add -f (systemd manages the process). BatchMode=yes makes ssh fail instead of waiting when it would prompt for a password or similar. When ServerAliveInterval detects a dropped connection and ssh exits, Restart=always starts it again after 10 seconds.
Use autossh
autossh starts ssh, watches it, and starts it again when it exits. On Debian and Ubuntu, install it with sudo apt install autossh.
autossh -M 0 -f -N -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" -o "ExitOnForwardFailure yes" -L 3307:127.0.0.1:3306 user@db.example.com
-M 0 tells autossh not to use its own monitoring port; instead, ssh detects the dropped connection with ServerAliveInterval, exits, and autossh restarts it.
Be careful with the listen address
If you omit the listen address, only your PC itself (127.0.0.1 and ::1) can connect to the forwarded port. If you write -L 0.0.0.0:3307:127.0.0.1:3306 or add -g, other devices on the same network can also connect to the forwarded port, which can expose a database or similar service unintentionally. Unless you need it, omit the listen address or specify 127.0.0.1 explicitly.
Listening on a port below 1024 usually requires administrator privileges. Use a port of 1024 or higher on your PC.
Restrict forwarding on the server
AllowTcpForwarding on the SSH server (OpenSSH) defaults to yes, so any user who can log in can use forwarding. If you create a user just for tunnels, you can narrow the allowed destinations at the end of /etc/ssh/sshd_config.
Match User tunnel
AllowTcpForwarding local
PermitOpen 127.0.0.1:3306
X11Forwarding no
PermitTTY no
AllowTcpForwarding local allows only local port forwarding (-L), and PermitOpen limits the destination to the given host and port. Forwarding to any other destination is refused with administratively prohibited. After the change, check the syntax with sudo sshd -t and then reload the SSH service (the service is named ssh on Ubuntu and Debian, and sshd on RHEL-based systems). A mistake can lock you out, so keep your current SSH session open and confirm that you can log in over a new connection.
Summary
- In
ssh -L listen_port:destination_host:destination_port user@server, write the destination as seen from the SSH server - Add
-Nfor forwarding only,-ffor background, andExitOnForwardFailure=yesso ssh exits when forwarding fails - Put tunnels you use often in
LocalForwardin~/.ssh/config, and detect drops withServerAliveInterval - To keep a tunnel running, use systemd's
Restart=alwaysor autossh, not a loop that keeps starting ssh - Keep the listen address at
127.0.0.1, and narrow destinations on the server withAllowTcpForwardingandPermitOpen
Related articles
- How to use SSH port forwarding (-L/-R/-D): from opening tunnels to server settings and autossh
- How to set up SSH public key authentication (from creating an ed25519 key to disabling password login)
- Securing SSH on a WordPress server: public keys, disabling passwords and Fail2ban
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)