panel v0.9.1 — open-source game server manager
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+116
@@ -0,0 +1,116 @@
|
||||
# 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. 7000–7999 / 8000–8999) 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.
|
||||
Reference in New Issue
Block a user