Guide · SSH
SSH tunnels on Mac: local, remote and dynamic forwarding
Updated · 6 min read
An SSH tunnel forwards a port through an encrypted SSH connection. On a Mac you create one from Terminal with the ssh that ships with macOS:
-L(local): a port on your Mac reaches a service on the server. Runssh -N -L 5433:localhost:5432 user@serverand connect to PostgreSQL atlocalhost:5433.-R(remote): a port on the server reaches a service on your Mac.-D(dynamic): a SOCKS proxy on your Mac that exits to the internet from the server.
On this page
Before you start
- SSH access to the server (keys preferred). If you already have an alias in
~/.ssh/config, use it instead ofuser@server(→ ssh config guide). - The server must allow forwarding (
AllowTcpForwarding, on by default in OpenSSH). -Nmeans "don’t run a remote command, just hold the tunnel". The tunnel lives as long as that Terminal window does.
Local forwarding (-L): reach a remote database
The syntax is -L local_port:destination:destination_port. The destination is resolved from the server, so localhost means the server itself.
PostgreSQL listening only on the server’s localhost
ssh -N -L 5433:localhost:5432 user@serverIn another tab:
psql -h localhost -p 5433 -U app mydbPort 5433 on the Mac avoids clashing with a local PostgreSQL on 5432. GUI clients (TablePlus, DBeaver, Postico) take the same settings: host localhost, port 5433.
MySQL or MariaDB
ssh -N -L 3307:127.0.0.1:3306 user@servermysql -h 127.0.0.1 -P 3307 -u app -pUse 127.0.0.1, not localhost. With localhost the MySQL client tries a local Unix socket and skips the tunnel entirely.
A database on another machine in the private network
If the database runs on db.internal and only the server can reach it:
ssh -N -L 5433:db.internal:5432 user@serverIn the background
ssh -f -N -o ExitOnForwardFailure=yes -L 5433:localhost:5432 user@server-f backgrounds ssh after authentication, and ExitOnForwardFailure=yes makes it fail instead of sitting there without a tunnel. To close it:
pkill -f "5433:localhost:5432"Remote forwarding (-R): expose a port from your Mac
Syntax: -R remote_port:destination:destination_port. This time the destination is resolved from your Mac.
ssh -N -R 8080:localhost:3000 user@serverOn the server, curl http://localhost:8080 hits the app on port 3000 of your Mac. Handy for receiving a webhook during development, or letting a server process call a local service.
By default the remote port only listens on the server’s loopback. To open it to other machines, the server admin has to set GatewayPorts clientspecified in /etc/ssh/sshd_config, and you pick the interface:
ssh -N -R 0.0.0.0:8080:localhost:3000 user@serverOnly do this if you know who can reach that port.
Dynamic forwarding (-D): a SOCKS proxy
ssh -N -D 1080 user@serverThis opens a SOCKS5 proxy on port 1080 of your Mac; traffic leaves from the server. Test it:
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.meYou should get the server’s IP. To route your Mac’s apps through it:
networksetup -setsocksfirewallproxy "Wi-Fi" 127.0.0.1 1080
networksetup -setsocksfirewallproxystate "Wi-Fi" off # to turn it offReplace "Wi-Fi" with your interface name (networksetup -listallnetworkservices).
Make tunnels permanent in ~/.ssh/config
Host prod-db
HostName server.example.com
User user
LocalForward 5433 localhost:5432
ServerAliveInterval 30
ExitOnForwardFailure yesNow ssh -N prod-db opens the tunnel. The equivalents of -R and -D are RemoteForward 8080 localhost:3000 and DynamicForward 1080.
Add a tunnel to a connection that’s already open
With ControlMaster (a reusable master connection) you can add and remove forwards without opening another session:
ssh -O forward -L 5433:localhost:5432 prod-db
ssh -O cancel -L 5433:localhost:5432 prod-dbCommon errors
bind [127.0.0.1]:5433: Address already in use. Something on your Mac already holds that port. Find it withlsof -nP -iTCP:5433 -sTCP:LISTEN, then pick another port or stop that process.channel 2: open failed: connect failed: Connection refused. The tunnel is up, but nothing is listening ondestination:portas seen from the server. On the server, check withss -ltnthat the database listens on that port and interface.administratively prohibited: open failed. The server hasAllowTcpForwarding no. Only the server admin can change that.Warning: remote port forwarding failed for listen port 8080. With-R: the port is already taken on the server, or remote forwarding isn’t allowed.- The MySQL client ignores the tunnel. You’re using
-h localhost; switch to-h 127.0.0.1. - The tunnel drops after a while idle. A router or firewall is killing idle connections. Add
ServerAliveInterval 30. Privileged ports can only be forwarded by root. Ports below 1024 on your Mac need root. Use a high port (5433, 8080…).
With Terminalia
Doing it with Terminalia
Terminalia adds local (-L) and remote (-R) forwards from a form and toggles them live on the same SSH connection, so you don’t authenticate again. It shows the equivalent ssh command, tells you if a local port is taken and by which process, and imports the LocalForward and RemoteForward lines from your ~/.ssh/config. If the network drops, it brings your tunnels back on reconnect. Dynamic forwarding (-D) isn’t in the form; open it from the built-in terminal with the command above.
Free · no account · macOS 14+ · Apple Silicon and Intel
FAQ
What’s the difference between -L and -R?
With -L you listen on a port on your Mac and traffic goes to a service the server can reach: that’s how you get to a remote database. With -R a port on the server listens and traffic comes back to a service on your Mac, which is how you expose something local to the server.
Is it safe to tunnel into a production database?
Safer than exposing the database port to the internet. Traffic is encrypted inside SSH, and by default the local port only listens on 127.0.0.1, so other machines on your network can’t see it. Keep using a database user with only the permissions it needs.
How do I see which tunnels are open on my Mac?
List the ssh processes listening on local ports with lsof -nP -iTCP -sTCP:LISTEN | grep ssh. You’ll see each tunnel’s port and PID. To close one, kill that PID, or use ssh -O cancel if the tunnel hangs off a master connection.
Related guides
~/.ssh/config with examples
· 5 min
ProxyJump through a bastion
· 5 min