All tutorials
15 min read

How to Open a Private Dashboard with an SSH Tunnel (Port Forwarding)

Open a dashboard bound to 127.0.0.1 on your server through an SSH tunnel from Windows, macOS, or Linux, save it in ~/.ssh/config, and keep it alive in 6 steps.

TL;DR:

  • To open a dashboard that listens only on the server's 127.0.0.1, run ssh -N -L 8080:127.0.0.1:8080 deploy@YOUR_SERVER_IP on Windows 10 and 11, macOS or Linux, then browse to http://localhost:8080.
  • The dashboard port stays closed in UFW. The SSH tunnel carries its traffic, so the server needs only the SSH port (22/tcp by default).
  • ss -tln shows what each service binds: 127.0.0.1 is private, while 0.0.0.0, [::] and * mean every interface. Docker's -p 3000:3000 binds every interface by default and bypasses UFW.
  • Save forwards as LocalForward lines in ~/.ssh/config with ServerAliveInterval 30, and restart dropped tunnels with autossh 1.4g.
  • An ssh -L tunnel is not a VPN: it forwards TCP connections for the ports you name, while WireGuard routes IP packets for whole networks.
  • Keep -D and -R bound to localhost. A 0.0.0.0 bind turns -D into an open proxy, and GatewayPorts yes publishes an -R port on the server's public address.

Applies to: Ubuntu 24.04 LTS and 26.04 LTS servers (OpenSSH 9.6 and 10.2) · OpenSSH client on Windows 10 and 11, macOS 26 (OpenSSH 10.3) and Linux · PuTTY 0.85 · autossh 1.4g · checked October 2026

An SSH tunnel is an encrypted channel inside an SSH connection that carries TCP traffic between a port on your computer and an address on the server; OpenSSH calls the feature port forwarding. This tutorial opens dashboards that listen only on the server's loopback address, such as Open WebUI, the OpenClaw Control UI and wg-easy, with one ssh -L command, saves it in ~/.ssh/config and keeps it connected. It also covers the Docker port trap, reverse and dynamic tunnels, and when HTTPS or WireGuard fits better.

What is an SSH tunnel?

In an SSH tunnel, one end listens on a port, and each connection to it travels through the encrypted session and leaves the other end as a new TCP connection. OpenSSH has three forward types:

TypeFlagListens onConnects toTypical use
Local-LYour computerAn address as the server sees itA dashboard on the server's 127.0.0.1
Remote-RThe serverAn address as your computer sees itLet the server reach an app on your laptop
Dynamic-DYour computer, as a SOCKS proxyWhatever each app requests, from the serverBrowse out through the server

Is an SSH tunnel a VPN?

Not in the port-forwarding form this tutorial uses. A local forward carries TCP connections for the ports you list, from the computer that runs ssh, and leaves the rest of your traffic unchanged. A VPN such as WireGuard adds a network interface (wg0) and routes IP packets, TCP and UDP, for whole subnets or all of a device's traffic. OpenSSH's -w option can forward tun devices as a VPN, and sshd's PermitTunnel setting turns it off by default.

SSH tunnel (ssh -L)WireGuard VPN
CarriesTCP connections to the ports you nameIP packets for the subnets you route
Port open on the serverThe SSH port51820/udp in the WireGuard tutorial
Server setupNonePackage, keys and a peer per device

Prerequisites

  • A Linux server, such as Ubuntu 24.04 LTS or 26.04 LTS, with a dashboard that listens on 127.0.0.1. Arct Cloud Linux VPS images include root SSH and accept an SSH public key at deploy.
  • A sudo user that logs in with an SSH key, called deploy here. See How to Generate an SSH Key on Windows, macOS, and Linux.
  • An SSH client: OpenSSH in Windows 10 (build 1809 or later) and 11, macOS and Linux, or PuTTY on Windows.

Step 1 runs on the server; Steps 2 to 6 run on your computer. Replace values in capitals, such as YOUR_SERVER_IP, with your own.

Step 1: Check what the dashboard listens on

The address a service binds decides whether it is private. On the server, list the listening TCP sockets:

ss -tln

The output looks similar to this:

State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      2048       127.0.0.1:8080       0.0.0.0:*
LISTEN 0      511        127.0.0.1:18789      0.0.0.0:*
LISTEN 0      4096         0.0.0.0:3000       0.0.0.0:*
LISTEN 0      4096            [::]:3000          [::]:*

In the Local Address column, 127.0.0.1 or [::1] is the loopback address: only programs on the server, and your tunnel, can connect. 0.0.0.0, [::] or * means every interface, so port 3000 above answers on the public IPv4 address, and on IPv6 if your server has a public IPv6 address. sudo ss -tlnp adds each socket's process.

Fix a Docker port published on every interface

Docker's documentation states that publishing container ports is insecure by default. A mapping such as "3000:3000" binds 0.0.0.0 and [::], and Docker routes it through NAT rules that run before the chains UFW uses, so a UFW deny rule does not close it. Put the loopback address in front of the mapping in compose.yaml:

    ports:
      - "127.0.0.1:3000:3000"

Recreate the container with docker compose up -d, and ss -tln shows 127.0.0.1:3000. The Docker installation tutorial sets a loopback default for new ports in daemon.json. Before Docker Engine 28.0.0, hosts on the same layer-2 segment could reach ports published on 127.0.0.1, so keep Docker updated.

Step 2: Open the dashboard with ssh -L

ssh -L listens on a port on your computer and sends each connection through the SSH session. Run this in Terminal on macOS and Linux or in PowerShell on Windows:

ssh -N -L 8080:127.0.0.1:8080 deploy@YOUR_SERVER_IP

If PowerShell reports that ssh is not recognized, add the OpenSSH Client optional feature.

  • -L 8080:127.0.0.1:8080 reads as local port, destination address, destination port. The server resolves the destination, so 127.0.0.1 is the server's own loopback address.
  • -N opens no remote shell. ssh prints nothing after login and holds the tunnel until you press Ctrl+C.

Open http://localhost:8080 in your browser. Use Chrome or Firefox for tunnel addresses; both accept secure cookies on http://localhost, which the Open WebUI login from the Ollama tutorial needs. Port 8080 stays closed on the server's public address.

ssh binds the local end to your computer's loopback address, because the client setting GatewayPorts defaults to no. Do not add -g or a bind address such as 0.0.0.0:8080:127.0.0.1:8080. Both let other devices on your network into the dashboard through your tunnel.

The port numbers can differ: -L 18080:127.0.0.1:8080 serves the dashboard at http://localhost:18080 when 8080 is taken locally. Pick a local port above 1023, as in this example. Keep 8080 for Open WebUI from the Ollama tutorial, whose CORS_ALLOW_ORIGIN lists http://localhost:8080.

Open an SSH tunnel on Windows with PuTTY

  1. On the Session panel of PuTTY 0.85, enter the server's IP address in Host Name.
  2. Open Connection > SSH > Tunnels. Enter 8080 in Source port and 127.0.0.1:8080 in Destination, keep Local selected, and click Add.
  3. Open Connection > SSH > Auth > Credentials and select your .ppk file under Private key file for authentication. To convert an OpenSSH key, see How to Generate an SSH Key.
  4. Back on Session, type a name under Saved Sessions, click Save, then Open, and log in as deploy.

PuTTY starts the forward after login and logs a line similar to Local port 8080 forwarding to 127.0.0.1:8080 in its Event Log. Leave both checkboxes under Port forwarding unchecked, so only your computer can use the forwarded port.

Step 3: Forward several ports in one command

Press Ctrl+C to close the Step 2 tunnel, then repeat -L once per dashboard. One SSH connection carries all of them:

ssh -N \
  -L 8080:127.0.0.1:8080 \
  -L 18789:127.0.0.1:18789 \
  -L 51821:127.0.0.1:51821 \
  deploy@YOUR_SERVER_IP

In PowerShell, put the command on one line; a trailing backslash does not continue a line there. These are the loopback ports in the tutorials on this site:

AppServer addressOpen on your computerTutorial
Open WebUI127.0.0.1:8080http://localhost:8080Ollama with Open WebUI
OpenClaw Control UI127.0.0.1:18789http://127.0.0.1:18789/Install OpenClaw
OpenCode web UI127.0.0.1:49374The link opencode pair printsInstall OpenCode
wg-easy127.0.0.1:51821http://localhost:51821WireGuard VPN server
Nginx Proxy Manager admin panel127.0.0.1:81http://localhost:8081, with -L 8081:127.0.0.1:81Nginx Proxy Manager with Docker
LiteLLM Admin UI127.0.0.1:4000http://localhost:4000/uiLiteLLM Proxy with Docker Compose
TeamSpeak ServerQuery127.0.0.1:10022ssh -p 10022 [email protected]TeamSpeak 6 server with Docker

The tunnel controls who can reach each page, and each app still asks for its own login.

Step 4: Save the tunnel in ~/.ssh/config

Put the tunnel's settings under one name in your SSH config file, ~/.ssh/config on macOS and Linux or C:\Users\YOUR_USER\.ssh\config (no file extension) on Windows. Place the block above any Host * block:

Host vps-dash
    HostName YOUR_SERVER_IP
    User deploy
    IdentityFile ~/.ssh/id_ed25519
    LocalForward 8080 127.0.0.1:8080
    LocalForward 18789 127.0.0.1:18789
    LocalForward 51821 127.0.0.1:51821
    ExitOnForwardFailure yes

LocalForward takes the local port, a space, then address:port. ExitOnForwardFailure yes makes ssh quit when it cannot open a forward, such as when a local port is in use, instead of connecting without it. Close the Step 3 tunnel with Ctrl+C, then open this one:

ssh -N vps-dash

Keep this alias apart from your shell-login alias: a second login with the same LocalForward lines tries to bind ports the tunnel already holds. ssh -G vps-dash prints the parsed block without connecting, including localforward 8080 [127.0.0.1]:8080.

Step 5: Keep the tunnel alive

ssh sends no server alive messages through the encrypted channel by default (ServerAliveInterval 0), so a dropped network path can leave a tunnel that looks open and carries nothing. Add two lines to the vps-dash block:

    ServerAliveInterval 30
    ServerAliveCountMax 3

ssh now asks the server for a response after 30 seconds without data. After 3 unanswered requests, about 90 seconds, it prints Timeout, server 203.0.113.10 not responding. and exits.

Restart the tunnel with autossh

autossh starts ssh again when the connection drops. Install it on your computer with sudo apt install autossh on Ubuntu and Debian or brew install autossh on macOS. On Windows, rerun ssh -N vps-dash after a drop.

autossh -M 0 -N vps-dash

-M 0 turns off autossh's monitoring port and relies on the ServerAlive lines to make ssh exit. Load your key into ssh-agent first, so no passphrase prompt blocks a restart. If the first ssh run fails within 30 seconds, autossh treats it as a setup error and stops.

To run a plain tunnel in the background on macOS or Linux, use ssh -f -N vps-dash. Stop it with pkill -f "ssh -f -N vps-dash".

Step 6: Verify the tunnel and the closed port

Close any tunnel from Steps 2 to 5 first: press Ctrl+C in its window, or run pkill -f "ssh -f -N vps-dash" for a background tunnel. Start the tunnel with -v to see each forward open:

ssh -v -N vps-dash

The output includes lines similar to these:

...
debug1: Local connections to LOCALHOST:8080 forwarded to remote address 127.0.0.1:8080
debug1: Local forwarding listening on ::1 port 8080.
...
debug1: Local forwarding listening on 127.0.0.1 port 8080.
...

Load http://localhost:8080, and ssh logs Connection to port 8080 forwarding to 127.0.0.1 port 8080 requested. Then test the server's public address from your computer:

curl -m 5 http://YOUR_SERVER_IP:8080

In PowerShell, type curl.exe, since curl is an alias there. The request times out after 5 seconds or is refused. A page or an HTTP status code means the port is public: go back to Step 1.

Keep reverse and dynamic tunnels on localhost

A dynamic forward (-D) turns ssh into a SOCKS4 and SOCKS5 proxy on your computer. Apps that use it connect out from the server, so websites see the server's IP address:

ssh -N -D 127.0.0.1:1080 deploy@YOUR_SERVER_IP

Set the browser's SOCKS v5 proxy to 127.0.0.1, port 1080. With -g, 0.0.0.0: or *:, any device that reaches your computer can relay traffic through your server, which makes it an open proxy. Use the proxy only for lawful purposes and within your provider's acceptable use policy.

A remote forward (-R) opens a port on the server that leads back to your computer:

ssh -N -R 9000:127.0.0.1:3000 deploy@YOUR_SERVER_IP

Only programs on the server can reach your local port 3000, at 127.0.0.1:9000, because sshd binds remote forwards to loopback while GatewayPorts is no, the default. Check the active values on the server:

sudo sshd -T | grep -E '^(gatewayports|allowtcpforwarding|disableforwarding) '

With OpenSSH's default settings, the output is:

gatewayports no
allowtcpforwarding yes
disableforwarding no

GatewayPorts yes, or clientspecified with a request such as -R 0.0.0.0:9000:127.0.0.1:3000, publishes your local service on the server's public address. If you never use -R, allow local forwards only; -L and -D keep working:

echo 'AllowTcpForwarding local' | sudo tee /etc/ssh/sshd_config.d/10-forwarding.conf
sudo sshd -t
sudo systemctl restart ssh

sshd's manual notes that users with shell access can still run their own forwarders, so this setting limits sshd only.

When to use a reverse proxy or WireGuard instead

OptionFits whenOpens on the serverGuide
SSH tunnelOne admin opens a dashboard now and thenNothing beyond the SSH portThis tutorial
Reverse proxy with HTTPSOther people, phones or outside services need the app on a domain80/tcp and 443/tcpNginx Proxy Manager with Docker
WireGuard VPNSeveral devices, including phones, reach several private services51820/udpWireGuard VPN server

Webhooks need a public HTTPS address, so the n8n tutorial uses a reverse proxy. The Nginx, PostgreSQL, and Redis guide for Ubuntu 24.04 sets up Nginx as a reverse proxy for an app on 127.0.0.1:3000. Admin pages such as the OpenClaw Control UI stay on loopback.

Troubleshoot common SSH tunnel errors

Run ssh with -v as in Step 6. On the server, sudo journalctl -u ssh shows what sshd refused.

SymptomCauseFix
bind [127.0.0.1]:8080: Address already in use, then Could not request local forwarding.Another program or an earlier tunnel holds port 8080 on your computerClose it, or pick a free local port such as -L 18080:127.0.0.1:8080. For Open WebUI, also add http://localhost:18080 to CORS_ALLOW_ORIGIN
channel 2: open failed: connect failed: Connection refused (the channel number varies) when the page loadsNothing listens on that server port, or the service binds another addressRun Step 1 and match the -L destination to the ss output
channel 2: open failed: administratively prohibited: open failedsshd refuses TCP forwarding (AllowTcpForwarding no or remote, or DisableForwarding yes)Remove DisableForwarding yes and any other AllowTcpForwarding line from /etc/ssh/sshd_config and /etc/ssh/sshd_config.d/, set AllowTcpForwarding local in 10-forwarding.conf, run sudo sshd -t and sudo systemctl restart ssh, then check the values with sudo sshd -T
Timeout, server 203.0.113.10 not responding.The Step 5 server alive requests got no answerReconnect, or run the tunnel under autossh
Warning: remote port forwarding failed for listen port 9000The server port is in use, or sshd refuses -RPick another port, or check sudo sshd -T

FAQ

Port forwarding is a built-in, documented feature of OpenSSH, and this tutorial uses it to reach a server you control. What you send through a tunnel must be lawful where you are and must follow the acceptable use rules of your provider and of the network you connect from.

What is the difference between SSH and SSH tunneling?

SSH is the encrypted protocol and client you use to log in and run commands on a server. SSH tunneling, also called port forwarding, uses the same connection to carry other TCP traffic, such as a browser session to a dashboard on the server.

How do I create an SSH tunnel on Windows?

Run ssh -N -L 8080:127.0.0.1:8080 deploy@YOUR_SERVER_IP in PowerShell. Windows 10 (build 1809 or later) and Windows 11 include OpenSSH Client as an optional feature. In PuTTY, add the forward under Connection > SSH > Tunnels before you click Open.

Can one SSH tunnel forward multiple ports?

Yes. Repeat -L for each port in one ssh command, or add one LocalForward line per port to a Host block in ~/.ssh/config. All forwards share one SSH connection and close together.

How do I use an SSH tunnel through a jump host?

Add -J with the jump host: ssh -N -J deploy@YOUR_JUMP_HOST -L 8080:127.0.0.1:8080 deploy@YOUR_TARGET_HOST. The 127.0.0.1 in -L then refers to the target server. In ~/.ssh/config, a ProxyJump line does the same.

Can I use an SSH tunnel as a proxy?

Yes. ssh -N -D 127.0.0.1:1080 deploy@YOUR_SERVER_IP starts a SOCKS proxy on your computer, and sites see the server's IP address. Keep it bound to 127.0.0.1 and use it only for lawful purposes.

What is a reverse SSH tunnel?

A reverse tunnel (-R) opens a port on the server that leads back to a service on your computer. sshd binds it to the server's loopback address while GatewayPorts is no, the default. GatewayPorts yes publishes that service on the server's public address.

Next steps

Arct Cloud Linux images deploy in under 30 seconds with root SSH on every plan. Compare plans.

This work is licensed under CC BY-NC-SA 4.0.