Files
panel/docs/PAIRING.md
T
2026-07-14 23:37:25 -07:00

4.6 KiB
Raw Blame History

Agent pairing (mTLS)

The controller runs an internal CA (--ca-dir, default ./data/ca, auto-generated on first run). Every agent gets a client certificate signed by that CA via a one-time pairing token. After pairing, all controller↔agent gRPC traffic is mutually-authenticated TLS, and any "agent" without a CA-signed cert is rejected during the TLS handshake — it never reaches application code.

install.sh / install.ps1 pair the local agent for you. Do this by hand only for additional hosts or manual installs.

The flow

1. Mint a one-time token (admin)

Dashboard: Agents view → Pair agent → optional description + expiry (default 15 minutes) → Generate. The token is displayed exactly once — only its SHA-256 hash is stored, so copy it now.

Or curl (with an authenticated admin session cookie):

curl -X POST https://panel.example.com/api/admin/pair-tokens \
  -H 'Content-Type: application/json' \
  -b "panel_session=$SESSION" \
  -d '{"description":"gamebox-2","expires_minutes":15}'
# → {"id":"pt_…","token":"…copy me…","expires_at":"…"}

(GET /api/admin/pair-tokens lists outstanding tokens.)

2. Run the agent once in pair mode (new host)

./bin/agent \
  --pair-url  https://panel.example.com \
  --pair-token 'THE_TOKEN' \
  --agent-id  gamebox-2 \
  --cert-dir  ./data/certs

What happens (agent/cmd/agent/pair.go):

  1. Fetches the controller CA cert from GET /api/pair/ca. This first fetch skips TLS verification — the one-time token is what gates actual cert issuance.
  2. Generates an RSA-2048 keypair locally — the private key never leaves the host.
  3. Builds a CSR with CN = the agent id and POSTs {token, agent_id, csr_pem} to POST /api/pair/issue, this time verifying the server against the CA it just fetched.
  4. Writes ca.crt, agent.crt, agent.key (key mode 0600) into --cert-dir and exits.

--pair-url is the controller's HTTP dashboard port (8080 / your reverse proxy), not the gRPC port.

3. Run the agent normally

./bin/agent --controller panel.example.com:8443 \
  --modules-dir ./modules --data-root ./data/instances \
  --meta-dir ./data/instance-meta --backup-dir ./data/backups \
  --cert-dir ./data/certs

The agent auto-detects the cert files in --cert-dir and dials with mTLS. Its identity (--agent-id default) is the cert CN. No --insecure anywhere.

Multi-host setup

  • Controller box: one controller + Postgres. Agents dial out to gRPC :8443, so agent boxes need no inbound panel ports at all.
  • Each game box: copy (or clone) the repo's modules/ directory and the agent binary, pair once (steps above), run under systemd (deploy/systemd/panel-agent.service — set --controller <controller-host>:8443).
  • Port windows: give each agent a disjoint port range in the agent edit modal (e.g. 70007999 / 80008999) so instance host ports never collide in your router's forwarding rules.
  • Hostnames: the controller's server cert covers its hostname/IPs at generation time. If agents will dial a DNS name (or a reverse-proxied public name), start the controller with -tls-hostnames panel.example.com before pairing so the SAN is in the cert.

panelctl uses the same certs: it defaults to mTLS with the certs in --cert-dir and falls back to plaintext only against --insecure controllers.

How rogue agents are rejected

  • The controller's gRPC listener (in -tls auto|on mode) requires a client certificate signed by its internal CA. A connection without one fails the TLS handshake — before any RPC is served.
  • Pair tokens are one-time, short-lived (default 15 min), and stored only as hashes; a leaked expired token is useless, and a used token can't be replayed.
  • The agent's identity is the certificate CN, so an agent can't impersonate another agent id without a cert bearing that CN.
  • Compromised host? Delete its cert material and stop trusting it — and treat any secrets that agent held (instance data) as exposed.

Troubleshooting

  • connection refused in pair mode--pair-url points at gRPC :8443; it must be the HTTP port (:8080 or your proxy URL).
  • TLS handshake errors after pairing — controller cert doesn't cover the name the agent dials. Re-run the controller with -tls-hostnames, delete data/ca server cert only if you know what you're doing (re-pairing all agents), or dial by the covered IP.
  • Dev shortcut--insecure on the agent + -tls off on the controller skips all of this on a trusted single box. Never on a network you don't own.