Reach a private service on your VPS with an SSH tunnel
Open an admin page or database that listens only on the server's localhost, from your own computer, through SSH. Nothing new is exposed to the internet.
Tested on Ubuntu 24.04 LTS
Before you start
- A server you can already log in to with SSH
- A service on the server listening on 127.0.0.1 (an admin page or a database)
- An SSH client on your computer (built into Windows 10 and 11
- macOS and Linux)
To reach a service that listens only on your server's localhost, run ssh -N -L 8080:127.0.0.1:8080 root@<server> on your own computer and open http://localhost:8080 in your browser. SSH carries the traffic over the connection you already use to log in, so the service stays closed to the rest of the internet: no port to open in the panel firewall or ufw.
It's the usual way to open an app's admin page before it has a domain and HTTPS, to use a database client against a database that only listens locally, or to look at a dashboard you never want public.
What we tested: on 11 November 2026 we ran every command here from Windows 11 (its built-in OpenSSH 9.5) and from Linux (OpenSSH 10.2) against an Ubuntu 24.04 server with a web page listening only on 127.0.0.1:8080. Through the tunnel it answered; from another machine on the same network, a direct connection to that port failed. We also triggered both errors below on purpose.
How it works
-L 8080:127.0.0.1:8080 means: listen on port 8080 on my computer, and send whatever arrives there through SSH to 127.0.0.1:8080 as seen from the server. So 127.0.0.1 in the middle is the server's own localhost, not yours.
The first number is your choice. Pick any free port above 1024 on your computer; it doesn't have to match the server's.
Open the tunnel
Find the port the service listens on, on the server:
sudo ss -ltnpA line like 127.0.0.1:8080 ... users:(("python3",...)) means the service listens on port 8080, on localhost only. That's what a tunnel is for.
Then, on your computer (PowerShell or Windows Terminal on Windows, Terminal on macOS or Linux), with your server's address:
ssh -N -L 8080:127.0.0.1:8080 root@203.0.113.42-Nopens the tunnel without starting a shell. The command just waits; that's normal.- Use your usual user instead of
rootif you log in as one, and add-i path/to/keyif your key isn't the default one.
Leave that window open and browse to http://localhost:8080. If the service uses HTTPS with its own certificate, use https://localhost:8080 and expect the browser to warn about the certificate: it was made for the server, not for localhost.
Close the tunnel with Ctrl+C in that window.
A database client
The same works for a database that listens only locally. For PostgreSQL on its usual port, with a local port that won't clash with a PostgreSQL on your own computer:
ssh -N -L 15432:127.0.0.1:5432 root@203.0.113.42Then point your client at host localhost, port 15432, with the database's own user and password.
Several at once
Repeat -L:
ssh -N -L 8080:127.0.0.1:8080 -L 15432:127.0.0.1:5432 root@203.0.113.42Keep it to yourself
By default the tunnel's end on your computer listens on localhost only, so other machines on your Wi-Fi can't use it. Don't add -g or a 0.0.0.0: address in front of the first port unless you mean to share it.
The service on the server should stay bound to 127.0.0.1. If you later want it public, give it a domain and HTTPS behind a reverse proxy instead, and open only ports 80 and 443.
If it doesn't work
bind [127.0.0.1]:8080: Address already in use, then Could not request local forwarding: something on your computer already uses that port, often an earlier tunnel you left open. Choose another first number (-L 8081:127.0.0.1:8080) or close the other one. Add -o ExitOnForwardFailure=yes and SSH stops with an error instead of carrying on without the tunnel.
channel 2: open failed: connect failed: Connection refused in the SSH window when you load the page: the tunnel is fine, but nothing on the server listens on that port. Check the port with sudo ss -ltnp, and that the service is running (systemctl status <service>, or docker ps for a container).
The service listens on another address: if ss -ltnp shows it on something other than 127.0.0.1, such as a Docker container's address, put that address in the middle instead, for example -L 8080:172.17.0.2:8080.
On Windows, WARNING: UNPROTECTED PRIVATE KEY FILE! and bad permissions: Windows OpenSSH ignores a key file other users can read. In PowerShell, with your key's path:
icacls C:\Users\you\.ssh\id_ed25519 /inheritance:r /grant:r "$($env:USERNAME):F"Permission denied (publickey): the tunnel uses your normal SSH login, so fix that first: Connect to your server with SSH.
What we didn't test
macOS's SSH client (the same OpenSSH options apply), PuTTY's tunnel settings, and remote forwards (-R), which expose a port the other way round and aren't needed for this.