> ## Documentation Index
> Fetch the complete documentation index at: https://jephalabs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Operator Network Access

> Configure network access and firewall rules for QTG operators

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

```
127.0.0.1:8100:8100   # QTG backend
127.0.0.1:3000:3000   # QTG dashboard
127.0.0.1:8110:8110   # CCIP sidecar
```

This is a **default-deny posture**: a port bound to `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](/runbooks/dashboard-auth-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:

```bash theme={null}
ssh -N -L 8100:localhost:8100 -L 3000:localhost:3000 <docker-host>
```

With this tunnel open on your workstation:

* Dashboard: `http://localhost:3000`
* Backend API: `http://localhost:8100`

The `-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`:

```
Host qtg-tunnel
  HostName <docker-host>
  LocalForward 8100 localhost:8100
  LocalForward 3000 localhost:3000
  ServerAliveInterval 60
```

Then: `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 in `docker-compose.yml`** (override the default `127.0.0.1`):

```yaml theme={null}
ports:
  - "100.x.y.z:8100:8100"   # replace with actual Tailscale IP
  - "100.x.y.z:3000:3000"
```

Or use a `docker-compose.override.yml` on the server to keep the canonical compose file
unchanged:

```yaml theme={null}
# docker-compose.override.yml (not committed)
services:
  qtg-mainnet:
    ports:
      - "100.x.y.z:8100:8100"
  qtg-dashboard:
    ports:
      - "100.x.y.z:3000:3000"
```

**Alternatively**, run the dashboard behind `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.json` before
  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](/runbooks/dashboard-auth-v1-to-hmac) for the key lifecycle.

***

## Recommended OSS deployment topology

```
Operator workstation
  └─ SSH tunnel or Tailscale
        └─ Docker host (127.0.0.1:8100, 127.0.0.1:3000)
              ├─ qtg-mainnet (backend, 8100)
              ├─ qtg-dashboard (nginx SPA, 3000)
              └─ postgres (127.0.0.1:5432, loopback only)
```

This topology keeps all QTG services off the public internet while giving
authorised operators full dashboard and API access.

***

## Further reading

Dashboard write authentication migrated from a shared bearer token to per-operator HMAC v3.
For the key model, the `MG_DASHBOARD_WRITE_TOKEN` removal/startup guard, and the migration
procedure, see:

> [Auth Migration: v1 to HMAC](/runbooks/dashboard-auth-v1-to-hmac)

> **Startup guard**: `main.py` hard-fails at boot if `MG_DASHBOARD_WRITE_TOKEN` is 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.yml` before starting the server.
> `VITE_DASHBOARD_WRITE_TOKEN` is likewise removed from the frontend bundle.
