Keep a Dashboard Private
Reach admin UIs, AI agent dashboards, and databases without exposing them: bind to localhost, then use an SSH tunnel, a tailnet, or a gated proxy.
Some things should never be on the public internet, even behind a login: the admin UI of an AI agent that can run commands, a database console, an internal wiki, a monitoring dashboard. The OpenClaw and Hermes Agent guides both say to keep their dashboards behind SSH or a tailnet, and this is how. Three approaches, in order of effort.
Step zero: bind to localhost#
Whatever you choose, first make the service listen only on the loopback interface so nothing outside the box can reach it directly. In Compose, publish "127.0.0.1:3000:3000" rather than "3000:3000"; in a native app, set its listen address to 127.0.0.1. Confirm with:
ss -tlnp | grep 3000 # should show 127.0.0.1:3000, not 0.0.0.0:3000
Belt and braces: leave the port closed in the firewall too.
Option 1: SSH tunnel (zero install)#
You already have SSH. Forward a local port on your computer to the service on the server:
ssh -N -L 3000:127.0.0.1:3000 [email protected]
# then open http://localhost:3000 in your browser
Make it a one-word command by adding to ~/.ssh/config:
Host agent
HostName 203.0.113.10
User alex
LocalForward 3000 127.0.0.1:3000
LocalForward 5432 127.0.0.1:5432
Now ssh -N agent opens both forwards. Best for one or two people who already SSH in; the session must stay open while you use the dashboard.
Option 2: a tailnet (WireGuard made easy)#
Tailscale (or its open-source cousin Headscale, or plain WireGuard) puts your server and your devices on a private network with stable addresses. The dashboard is then reachable from your laptop or phone at the server's tailnet address, always, with no port open to the internet.
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up # prints a login URL; authorise the machine
tailscale ip -4 # e.g. 100.101.102.103
Bind the service to that address as well as localhost (or use tailscale serve to proxy it with HTTPS inside the tailnet), and open http://100.101.102.103:3000 from any device on your tailnet. Access control lives in the tailnet's admin panel. Best for teams, phones, and always-on access.
Option 3: the reverse proxy, with a gate#
If a browser URL on the public internet is genuinely required, put the dashboard behind the reverse proxy with an additional layer the app itself does not provide: HTTP basic auth or a forward-auth identity provider, plus an IP allowlist if your locations are stable. In Caddy:
agent.example.com {
@allowed remote_ip 198.51.100.7 203.0.113.0/24
handle @allowed {
basic_auth {
alex $2a$14$...bcrypt hash from: caddy hash-password...
}
reverse_proxy 127.0.0.1:3000
}
respond 403
}
This is the weakest of the three, because a public hostname is a public target; use it only when the first two are impossible.
Which one?#
- Just you, occasionally: SSH tunnel.
- You, your phone, a colleague, daily: tailnet.
- A product for other people to log in to: reverse proxy, with the app's own auth plus 2FA, and a hard look at whether it needs to be public at all.
Whichever you pick, agents with terminal access get their own server, scoped credentials, and 2FA on every login that controls them; see Recover from a Compromised Server for what happens when that advice is skipped.
