Operator Network Access
How to reach the QTG backend (port 8100) and dashboard (port 3000) securely from
an operator workstation when Docker Compose binds only to 127.0.0.1.
Why localhost binding by default
infra/docker-compose.yml binds all service ports to 127.0.0.1 (loopback only):
0.0.0.0 is reachable from any host
that has network access to the Docker host, including other VMs on the same subnet, cloud
metadata services, and misconfigured firewall rules.
The assumed operator threat model is that the Docker host is a single dedicated server that
operators reach via SSH or Tailscale. Binding to 127.0.0.1 means no external traffic can
reach QTG unless an explicit tunnel or rebind is in place — reducing the blast radius of a
firewall misconfiguration from “full exposure” to “nothing exposed”.
Dashboard write authentication is now per-operator HMAC v3 (nonce + timestamp), not a shared
bearer token. The browser holds a non-extractable CryptoKey, so there is no static secret
baked into the bundle to extract, and replayed requests are rejected. Localhost-only binding
is therefore defense-in-depth — a network-layer backstop — rather than the primary access
control. See Auth Migration: v1 to HMAC for the
per-operator key model and migration steps.
Option 1: SSH tunnel (recommended for single-operator access)
Forward both ports over an existing SSH session to the Docker host:- Dashboard:
http://localhost:3000 - Backend API:
http://localhost:8100
-N flag keeps the tunnel open without opening a shell. Run it in a terminal
or use autossh for a persistent tunnel.
For long-lived production setups, add to ~/.ssh/config:
ssh -N qtg-tunnel
Option 2: Tailscale rebind
If the Docker host is enrolled in a Tailscale tailnet, you can rebind the ports to the Tailscale IP so all tailnet members can reach the services directly. Rebind indocker-compose.yml (override the default 127.0.0.1):
docker-compose.override.yml on the server to keep the canonical compose file
unchanged:
tailscale serve (refer to Tailscale’s own
documentation for tailscale serve setup — do not replicate it here).
Residual risk with Tailscale
- ACL discipline: every device on the tailnet can reach the rebind ports unless
Tailscale ACLs are tightened to tag-based rules. Audit
tailnet-policy.jsonbefore rebinding. - Operator offboarding: dashboard write access is now per-operator HMAC, so when a tailnet member is removed or an operator leaves, revoke that operator’s HMAC key rather than rebuilding a shared-token bundle. See Auth Migration: v1 to HMAC for the key lifecycle.
Recommended OSS deployment topology
Further reading
Dashboard write authentication migrated from a shared bearer token to per-operator HMAC v3. For the key model, theMG_DASHBOARD_WRITE_TOKEN removal/startup guard, and the migration
procedure, see:
Auth Migration: v1 to HMAC
Startup guard:main.pyhard-fails at boot ifMG_DASHBOARD_WRITE_TOKENis present in the environment — even when set to an empty value (MG_DASHBOARD_WRITE_TOKEN=). Remove the key entirely from your environment /.env/docker-compose.ymlbefore starting the server.VITE_DASHBOARD_WRITE_TOKENis likewise removed from the frontend bundle.