# 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.

Source: https://rack4.io/tutorials/ssh-tunnel-private-service · Updated: 2026-11-11

Difficulty: beginner · Time: about 10 minutes · Tested on: Ubuntu 24.04 LTS

Prerequisites:

- 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](https://rack4.io/help/server-firewall) or [ufw](https://rack4.io/help/firewall-with-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:

```bash
sudo ss -ltnp
```

A 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:

```bash
ssh -N -L 8080:127.0.0.1:8080 root@203.0.113.42
```

- `-N` opens the tunnel without starting a shell. The command just waits; that's normal.
- Use your usual user instead of `root` if you log in as one, and add `-i path/to/key` if 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:

```bash
ssh -N -L 15432:127.0.0.1:5432 root@203.0.113.42
```

Then point your client at host `localhost`, port `15432`, with the database's own user and password.

### Several at once

Repeat `-L`:

```bash
ssh -N -L 8080:127.0.0.1:8080 -L 15432:127.0.0.1:5432 root@203.0.113.42
```

## Keep 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:

```powershell
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](https://rack4.io/help/connect-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.
