Files
panel/docs/PAIRING.md
T
2026-07-14 19:19:43 -07:00

117 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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):
```bash
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)
```bash
./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
```bash
./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.