Skip to main content

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:
  1. Did this request really come from a registered system?
  2. Does this key have permission to perform this action?
  3. Was the request body tampered with in transit?
  4. Is this request a replay of a previous one?
In other words, this is not multi-tenancy — it’s a service-to-service command protection layer.

Who are the callers?

Examples of systems that act as callers:
  • signal-bot — creates movements (operator or agent)
  • dashboard — approves/rejects (operator)
  • ops-cli — retries/resumes/cancels (operator)
  • admin-cli — signer rotation + registry status control (admin)
  • read-monitor — status queries (operator)
These systems don’t see different data — they share the same control plane but differ only in what actions they are allowed to take. The exception is the 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-Id
  • X-QTG-Timestamp
  • X-QTG-Nonce
  • X-QTG-Signature
Canonical string:
  • PATH is the routed, percent-decoded path the server dispatches on — no origin, no query string.
  • QUERY is 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.
The caller computes HMAC-SHA256 over this canonical string and sends it as X-QTG-Signature. QTG verifies in this order:
  1. timestamp freshness check
  2. key_id lookup
  3. HMAC comparison
  4. nonce uniqueness check
  5. role check
  6. (agent keys only) namespace match check
The signature is verified before the role check on purpose. The other way round, an unauthenticated caller could probe any 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
To prevent this, a nonce ledger is required. QTG records (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 one role, 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 transfers
  • agent — propose movements within a single tenant namespace, read its own status
This replaced the older 6-”purpose” model (read/write/approval/operate/admin/all). The purpose column survives as a label only; the live authorization axis is roles, and the seed CLI accepts only --role.
Authority is a fixed route catalog, keyed by (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

The agent 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

The hmac_secret printed to stdout is only visible at this moment. Store it in a secure location immediately.
Key lifecycle management CLI:

Secret Storage Policy

Principles

  1. Complete isolation between environments: staging and prod use separate secrets. Never share them.
  2. Least privilege: each caller is issued a key with the narrowest role it needs (agent < operator < admin). Reserve admin for the admin CLI only.
  3. Store immediately on creation: the secret printed by the seed CLI is one-time only. Store it in a secure location immediately.
  4. No git commits: never commit .env files, 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):
    1. Check current keys with list_auth_keys
    2. Seed a new key for the same client (seed_auth_client --key-id {client}-{env}-v{N+1})
    3. Deploy the new secret to the caller (update .env + restart)
    4. Smoke test with the new key
    5. Revoke the old key with revoke_auth_key --key-id {old-key-id}
  • 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).