Control Plane Auth
What is this auth for?
QTG v3’s inbound auth is not a user login system — it is control plane access control. The questions it answers are:- Did this request really come from a registered system?
- Does this key have permission to perform this action?
- Was the request body tampered with in transit?
- Is this request a replay of a previous one?
Who are the callers?
Examples of systems that act as callers:signal-bot— creates movements (operatororagent)dashboard— approves/rejects (operator)ops-cli— retries/resumes/cancels (operator)admin-cli— signer rotation + registry status control (admin)read-monitor— status queries (operator)
agent role, which is additionally tenant-isolated to a single namespace (see The agent role and namespace isolation).
Authentication method
Required headers:X-QTG-Key-IdX-QTG-TimestampX-QTG-NonceX-QTG-Signature
PATHis the routed, percent-decoded path the server dispatches on — no origin, no query string.QUERYis the query exactly as sent: no leading?, no sorting, empty string if absent. Sorting before signing transmits bytes you did not sign, and every such request 401s.
X-QTG-Signature.
QTG verifies in this order:
- timestamp freshness check
key_idlookup- HMAC comparison
- nonce uniqueness check
- role check
- (agent keys only) namespace match check
key_id against any route and read that key’s role
off the 401-vs-403 split.
Why is a nonce needed?
With only a timestamp, it’s possible to “resend the same request within the 5-minute window.” For example:approve movement X- An attacker captures the same request and resends it 30 seconds later
(key_id, nonce) in the DB, and if the same combination arrives again it is rejected with 401 replay detected.
Role model
Authorization is a 3-role axis. Every API client key carries exactly onerole, constrained at the DB level to one of:
admin— full control (template/signer/executor mutation, registry, rotation)operator— approve/reject, retry/resume/cancel, reads, capital transfersagent— propose movements within a single tenant namespace, read its own status
This replaced the older 6-”purpose” model (Authority is a fixed route catalog, keyed byread/write/approval/operate/admin/all). Thepurposecolumn survives as a label only; the live authorization axis is roles, and the seed CLI accepts only--role.
(method, path) → allowed roles:
Reads are not automatically the loosest tier —
GET /v3/signers is admin+operator while GET /v3/movements is open to agents. The table prevents, for example, an agent key from approving its own proposal, or an operator key from mutating a signer registry entry.
The catalog is extended at boot. Pro registers its own routes (Stargate chain and path-baseline admin, auto-approve policies) through the same
(method, path) → roles mechanism, so an OSS deployment simply has fewer entries — never looser ones.The agent role and namespace isolation
Theagent role is the only one with tenant isolation. An agent key is bound 1:1 to a network-scoped authority. The control plane enforces that the authority / namespace in the request path matches the authority the key is bound to — a mismatch is rejected with 403 agent_namespace_mismatch before the handler runs. So even a leaked agent key can only act within its own namespace, and only to propose (approval still gates execution unless an auto-approve policy is in scope).
Storage structure
Keys and replay-protection state are backed by a DB-based registry:- a record per registered client system
- a record per issued key, looked up directly by
key_id - a nonce ledger that records seen
(key_id, nonce)pairs for replay defense
Two authentication channels
QTG has two independent authentication channels:
These two are separate channels and do not cross. Dashboard UI uses
/dashboard/* routes; programmatic callers like Hummingbot and signal-bot use /v3/* routes. Dashboard write keys have role=operator (the dashboard_writer label survives only for log clarity). The old MG_DASHBOARD_WRITE_TOKEN / VITE_DASHBOARD_WRITE_TOKEN bearer-token model has been removed — the server hard-fails at startup if MG_DASHBOARD_WRITE_TOKEN is set. See the dashboard auth migration runbook.
Caller Inventory & Naming Matrix
Naming Convention
Key ID pattern:{client-name}-{environment}
Client Name: stays identical across environments. Must match the
--client-name argument of the seed CLI.
HMAC Caller Matrix
There is no
read-only role: GET routes are reachable by operator (and many by agent). Use operator for read/monitor callers.
Seed procedure
Secret Storage Policy
Principles
- Complete isolation between environments: staging and prod use separate secrets. Never share them.
- Least privilege: each caller is issued a key with the narrowest role it needs (
agent<operator<admin). Reserveadminfor the admin CLI only. - Store immediately on creation: the secret printed by the seed CLI is one-time only. Store it in a secure location immediately.
- No git commits: never commit
.envfiles, seed artifacts, or any file containing a secret.
Storage guide by environment
Management by secret type
Rotation policy
- Scheduled rotation: once per quarter, or immediately upon suspected secret exposure
- HMAC key rotation (zero-downtime):
- Check current keys with
list_auth_keys - Seed a new key for the same client (
seed_auth_client --key-id {client}-{env}-v{N+1}) - Deploy the new secret to the caller (update
.env+ restart) - Smoke test with the new key
- Revoke the old key with
revoke_auth_key --key-id {old-key-id}
- Check current keys with
- Dashboard writer rotation: re-seed the operator’s HMAC key and revoke the old one — no frontend rebuild (the key is no longer baked into the bundle)
- Emergency rotation: execute immediately, accepting service interruption
One-line summary
QTG v3 auth is a layer that enforces “who can send what kind of command to this control plane” via HMAC + role + nonce (+ namespace isolation for agent keys).