Docs/ Security/ Panel access through an SSH tunnel

Panel access through an SSH tunnel

A panel nobody can reach from the Internet cannot be attacked from it.

Overview

Your apps are served by sh0 on ports 80 and 443. The panel, which controls all of them, does not have to be. In tunnel-only mode the panel listens on 127.0.0.1:9000 alone: the only way in is an SSH connection to the server, which your firewall already restricts and which you already protect with a key.

Why not a public panel domain

Every TLS certificate is published in public Certificate Transparency logs, so a panel domain such as panel.example.com is listed on crt.sh within minutes of its certificate being issued. Scanners read those logs: a public panel is found, then probed, whatever its password policy.

Install in tunnel-only mode

On a fresh server, as root, with your firewall already limited to SSH, 80 and 443:

bash
curl -fsSL https://get.sh0.dev | bash -s -- --harden --panel-tunnel-only

Before sh0 starts for the first time, the installer writes a systemd drop-in that binds the panel to 127.0.0.1, turns off the *.sh0.app registration (a proxy cannot reach a panel on localhost), and with --harden does not open port 9000 in ufw. It ends by printing the exact ssh command to paste on your own computer.

Warning
Your apps lose their free *.sh0.app addresses in this mode. The *.sh0.app proxy reaches the server on port 9000, which tunnel-only mode closes, so every app must be served on a domain of your own. If you or your developers have no domain, keep the default installation: the panel stays reachable on port 9000 and each app keeps its free address.
Note
The first owner account still requires the setup token, read on the server with sudo sh0 setup-token. Through the tunnel nobody else can reach the panel, but the rule stays the same everywhere.

Open the tunnel

1. On your own computer (not on the server), open the tunnel and leave the command running:

bash
ssh -N -L 9000:127.0.0.1:9000 root@SERVER_IP

2. Browse to http://localhost:9000. The traffic travels inside SSH: nothing crosses the network in clear, even though the address starts with http://. Ctrl+C closes the tunnel.

3. To avoid retyping it, add a block to ~/.ssh/config on your computer:

text
Host sh0-panel
    HostName SERVER_IP
    User root
    LocalForward 9000 127.0.0.1:9000
    SessionType none
    ExitOnForwardFailure yes

Then ssh sh0-panel opens the tunnel. SessionType none needs OpenSSH 8.7 or later; with an older client, remove that line and run ssh -N sh0-panel.

Common failures

SymptomCause and fix
ssh hangs, then "Connection timed out"SSH itself is filtered: your IP is not allowed by the provider firewall, or the server listens on another port (add -p PORT). The tunnel is not the problem.
"bind [127.0.0.1]:9000: Address already in use"Port 9000 is already taken on your computer, often by another tunnel. Use another local port: -L 9001:127.0.0.1:9000, then http://localhost:9001.
Browser: "connection refused" on localhost:9000The tunnel is not open (the ssh command has stopped), or sh0 is not running on the server: systemctl status sh0.
"channel 2: open failed: connect failed" in the ssh windowSSH works but nothing answers on 127.0.0.1:9000 on the server, or AllowTcpForwarding is off in sshd. Check ss -ltn | grep 9000 and sshd -T | grep allowtcpforwarding.

Switch an existing server

A server that already runs sh0 is switched by running the installer again with the same options. It keeps your apps, writes the drop-in, removes an existing ufw rule for 9000 (with --harden), and restarts sh0 once, which also withdraws its *.sh0.app address. Later re-runs of the installer, for an update, keep the server in tunnel-only mode as long as the drop-in exists.

bash
curl -fsSL https://get.sh0.dev | bash -s -- --harden --panel-tunnel-only

Your apps keep answering on their domains: they are served by Caddy on 80 and 443, which the switch does not touch. Expect a few seconds of interruption while sh0 restarts. If a panel domain is set, the installer stops before changing anything and tells you why (next section).

What the tunnel does not close

Warning
Tunnel-only mode moves the panel's own listener to 127.0.0.1. It does not remove a panel domain: if sh0 panel-domain shows one, Caddy keeps serving the panel on that domain over port 443, to the whole Internet. For a real tunnel-only setup, remove the domain. The installer refuses --panel-tunnel-only while a domain is set through sh0 panel-domain or SH0_PANEL_DOMAIN in the service environment, and requires sh0 1.10.6 or later; a domain configured some other way is not detected, so check it yourself.
bash
sudo sh0 panel-domain          # show
sudo sh0 panel-domain clear    # remove the route and the setting

Browser access without a tunnel

If a tunnel is not an option for your team, keep a panel domain but put Cloudflare Access (or an equivalent identity-aware proxy) in front of it, and restrict port 443 at the provider firewall to Cloudflare's IP ranges. Without that second step, Access is bypassed by connecting to the server's IP directly. That restriction applies to every app on the server: each of their domains must then be proxied by Cloudflare too, or it stops answering. Do not combine this with --panel-tunnel-only.

Check the result

On the server, the panel must listen on the loopback address only, no panel domain must be set, and sh0 doctor must report no failure. From another network, port 9000 must be closed.

bash
ss -ltn | grep ':9000'          # 127.0.0.1:9000 only
cat /etc/systemd/system/sh0.service.d/panel-tunnel-only.conf
sudo sh0 panel-domain            # Domain: not set
sudo sh0 doctor

Going back

To make the panel reachable on the server's address again, delete the drop-in, restart sh0 and reopen the port:

bash
rm /etc/systemd/system/sh0.service.d/panel-tunnel-only.conf
rm /etc/systemd/system/sh0.service.d/cloud-registration.conf   # only to get *.sh0.app back
systemctl daemon-reload && systemctl restart sh0
ufw allow 9000/tcp                                              # if the firewall is ufw

The rest of the host (SSH keys, firewall, fail2ban) is covered in Securing your server