A database or admin panel listens on localhost only, and that is right. Open it on your own screen with ssh -L without publishing the port.
Your database should listen on 127.0.0.1 and nothing else. Outsiders can't see it, so there is no password for them to guess. And then you need it on your laptop anyway, to read a table or grab a dump. Don't open the port in the firewall for that. An SSH tunnel sends the connection through the same channel you already use to log in.
In short.
ssh -N -L 9000:127.0.0.1:8000 user@serverpoints port 9000 on your computer at port 8000 on the server. Openhttp://127.0.0.1:9000in a browser and the service answers as if it ran on your machine, while the server port stays closed to the internet. Further down:-Dfor a SOCKS proxy,-Jfor a jump host, and two options that stop a tunnel from dying quietly.
Picture an internal service on your server: an admin panel, a database, Redis, a monitoring dashboard. It listens on 127.0.0.1 only (the "this machine itself" address), and that is deliberate: what cannot be reached cannot be attacked. You can see which address a port is bound to with ss, which we covered in the ss article:
ss -tln | grep ":8000"Look at the address in the fourth column. Our test service printed LISTEN 0 5 127.0.0.1:8000 0.0.0.0:*, which means "I only listen to my own machine". Now try to reach it from outside, from another computer, on the public address:
curl -m 6 http://203.0.113.10:8000/No answer. The request times out (exit code 28), and that's exactly what we want. Now we need to get in ourselves without opening the port to everyone else.
The -L (local) flag ties a port on your computer to an address that the server can see:
ssh -N -L 9000:127.0.0.1:8000 [email protected]Let's take it apart, since this is the line people get wrong most often:
-N means do not start a shell on the server, just hold the tunnel. The window will look hung with no prompt; that is normal, the tunnel works until you press Ctrl+C.9000 is the port on your computer. Any free one above 1024.127.0.0.1:8000 is where to go from the server's point of view. Here 127.0.0.1 means the server itself, not your laptop. This is the main source of confusion.[email protected] is the usual SSH login address.Leave that window open, open a second one and talk to port 9000 locally:
curl -i http://127.0.0.1:9000/The page came from the server, and port 8000 is still closed to the internet. Same trick for any other service. To reach PostgreSQL from a laptop, forward its port and point the client at the local end of the tunnel:
ssh -N -L 15432:127.0.0.1:5432 [email protected]In a second window psql -h 127.0.0.1 -p 15432 -U username dbname connects to the database on the server. We picked a different local port (15432) so it does not clash with a PostgreSQL that may already run on the laptop.
Two errors cover most cases, and each one tells you what is wrong.
The local port is taken. Either an old tunnel is still running or some other program owns that port. Add -o ExitOnForwardFailure=yes and ssh will fail loudly instead of sitting there with no tunnel at all:
Use another port, or hunt down the old ssh process and kill it.
Nothing listens at that address on the server. The tunnel is up, curl gets a dropped connection (exit code 56), and ssh itself prints in the window that holds the tunnel:
channel 2: open failed: connect failed: Connection refusedRead it as: ssh got to the server fine, but nothing answers on that port. Double-check the number, then run ss -tln on the server to see whether the service is up. The "Connection refused" here comes from the far end, not from your laptop.
A tunnel can die without saying a word: the home router dropped the session, the laptop went to sleep, and the ssh -N window still looks open while doing nothing. Three settings deal with that. Instead of a long command, write them in ~/.ssh/config (on Windows the file is C:\Users\name\.ssh\config):
From now on ssh -N db-tunnel starts the tunnel, and with -f it goes to the background. The lines mean this:
LocalForward is just -L in file form.ServerAliveInterval 30 pings the server every 30 seconds with an "are you there?". Three misses in a row (ServerAliveCountMax 3) and ssh closes the connection, so you notice the break right away rather than hours later.ExitOnForwardFailure yes makes ssh quit with an error if it can't grab the port.The -L flag covers most jobs, but ssh has three more tricks worth knowing:
Flag | What it does | When you need it |
|---|---|---|
| A port on your side leads to a port the server can see | A database, admin panel or monitoring on the server |
| A port on the server leads to a port on your side | Show a local service to a colleague or receive a webhook on a laptop |
| A local SOCKS proxy: an application's traffic goes through the server | Reach the internet from the server's address |
| Log in to a server through an intermediate (jump) host | Internal servers without a public address |
Here is -D at work. This command raises a proxy on port 1080 on your computer:
ssh -f -N -D 1080 [email protected]Any program that can use SOCKS now reaches the internet from the server's address. curl does it with --socks5-hostname, which also sends DNS lookups through the server:
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.meWhat came back was the server's address, 203.0.113.10, not our home one. Handy side effect: if the server sits in another country, you can see how a site looks from there.
For -J (ProxyJump) it is easier to describe both hosts in the config, because the -i flag from the command line is not passed to the intermediate host:
Now ssh inner logs in to jump on its own and from there to 10.0.0.5. The key does not have to be copied to the intermediate server: ssh builds the connection to the inner host on your computer, and the jump host merely relays bytes.
-L 0.0.0.0:9000:..., or everyone on your network will see the port.-L you can also reach other machines on the server's internal network. Give SSH access only to people you trust.AllowTcpForwarding in sshd_config. Check the effective values with sudo sshd -T | grep -Ei "allowtcpforwarding|gatewayports|permitopen".On our server:
gatewayports no means ports opened with -R listen on the server itself only. That's the sane default, so leave it alone unless you have a reason. The remaining protections for a fresh server are in the guide to securing a new VPS.
Run ssh -N -L local_port:127.0.0.1:server_port user@address and leave that window alone. Your own programs then connect to 127.0.0.1:local_port. Ctrl+C closes the tunnel.
-L takes a port on your computer to something the server can reach. -R goes the other way round: a port on the server leads back to you. -D gives you a SOCKS proxy, so an application's traffic leaves from the server.
Forward its port, say -L 15432:127.0.0.1:5432, then aim your client at 127.0.0.1:15432. The login and password are the usual database ones, and the firewall stays shut.
Describe it in ~/.ssh/config with ServerAliveInterval 30 and ExitOnForwardFailure yes, and start it with -f. If you want it to reconnect by itself after a drop, you need something that restarts ssh, such as a systemd service.
Yes, the traffic is encrypted. The risks are binding the local port to 0.0.0.0 and handing SSH access to people you don't trust, because a tunnel can reach everything the server can. Keep keys and users under control.
ssh -N -L local:127.0.0.1:remote user@server.127.0.0.1 in the middle means the server, not your computer.ServerAliveInterval, ExitOnForwardFailure and an entry in ~/.ssh/config make a tunnel reliable and short to call.-D gives a SOCKS proxy and -J routes through a jump host; bind ports to 127.0.0.1.