Skip to main content

Security Model

QTG is designed for a world where automated systems — trading bots, LLM agents, rebalancing scripts — initiate fund movements. The security model ensures that even if one of those systems is compromised or buggy, the blast radius is contained. No single component can unilaterally move funds.

Trust Boundary Overview

The key insight: agents can propose but only operators can approve. The signing key lives in your KMS, not in QTG’s memory.

Keys Like a Company

QTG’s default EVM signer uses AWS KMS in your own AWS account. This means:
  • QTG holds a key ID, never a private key. The key material never leaves the KMS hardware security module.
  • Signing happens via an API call to KMS. Even if the QTG server is fully compromised, the attacker gets the ability to request signatures — but only through the approval gate, and only while they maintain access.
  • Key rotation is a first-class operation with an audit trail. Rotating marks the old signer deprecated, which removes it from binding resolution for newly created movements. It does not reach into work already planned: for each signer-bound node the selected signer key is stored when the movement is created — approval later adds that signer’s address snapshot without re-resolving it — so a node created before the rotation still dispatches with the old signer afterwards. Marking a signer retired is a separate, gated step — among its checks it refuses while a committed nonterminal node still references that signer, matched by signer key and, where recoverable, by signer-address snapshot. Signer lifecycle administration (rotate, inventory, allowances, retire) is a QTG Pro surface.
  • Multi-signer bootstrap is supported for redundancy or overlap during rotation.
For local development or evaluation, QTG supports a 1Password-backed local signer that fetches keys on demand from your vault. A plain environment variable signer exists for development only — it holds the key in process memory and should never be used with real funds. Security Tradeoffs § Signing keys compares the three side by side against what an attacker can actually do — read files on disk, dump process memory, run code inside the QTG process, or steal your AWS credentials. The rows are what separates them; the summary above is not.
The plain environment variable signer (MG_LOCAL_SIGNER_KEY with backend plain) is for development only. It provides no protection if the server process is compromised. Migrate to AWS KMS before moving real funds.

Approval Gate

Every movement stops at PENDING_APPROVAL before any funds move. This is not optional — it is baked into the lifecycle. What this means in practice:
  • A trading bot can create 100 movements per second. None of them execute until approved.
  • An LLM agent operating via the MCP server can propose transfers within its authority binding. It cannot approve its own proposals.
  • A misconfigured script that accidentally passes the wrong amount still gets caught at the gate.

Auto-Approve Policies

For high-frequency operations where manual approval would be impractical, you can configure auto-approve policies. These policies are scoped:
  • By template — only movements using specific templates can be auto-approved.
  • By strategy — only movements from specific strategy identifiers.
  • With budget limits — per-interval caps on total approved volume.
Auto-approve does not remove the approval stage. It evaluates the policy automatically instead of waiting for a human. If no policy matches, the movement waits.

Outflow Velocity Cap

Every guard above answers “is this one movement correct?” The outflow velocity cap answers a different question: “is the total volume leaving this instance normal?” It is the aggregate brake under the per-movement guards — a per-token, rolling-window ceiling enforced at the approval/reservation chokepoint. When approving a movement would push its token past the configured ceiling, the approval is rejected (the movement stays PENDING_APPROVAL) and the token self-throttles for the rest of the window, with no operator action and no global flag. Other tokens, each under their own ceiling, are unaffected. This bounds runaway strategy code, a stolen strategy/API key, and operator fat-fingers — all of which can emit individually-valid movements that each pass every per-movement guard. The cap is a speed bump with an operator override, not a wall: an emergency withdraw that must exceed the cap is approved with cap_override: true on the authenticated approve action. The override is honored only for operator/admin roles and is always audited — an agent/strategy key can never set it, so the cap survives exactly the adversary it targets. Ceilings live in env, not the DB, so the API-surface adversary cannot raise them. Coverage is CEX-balance lanes (the live Upbit ↔ Bithumb + Binance withdrawal path); on-chain bridge lanes are out of scope by construction. See the Outflow Velocity Cap guide for configuration, breach behaviour, and the override path.

Address Allowlist

Destination addresses are checked at dispatch time, not just at creation time. This distinction matters:
  1. Operator registers address 0xABC... on the allowlist.
  2. A movement is created targeting 0xABC... and approved.
  3. Before dispatch, the operator removes 0xABC... from the allowlist.
  4. Dispatch fails with an address guard violation. No funds move.
This means revoking an address actually stops the next transfer, even if movements targeting that address are already approved and queued. The allowlist also distinguishes two action classes, because not every on-chain executor controls both ends of a transfer:
  • Destination-only (stargate_send, usdt0_send, lighter_secure_withdraw) — the source is bound to the signer, so only the destination is allowlist-checked.
  • Strict source + destination (cctp_burn/cctp_mint, gateway_*, ccip_send, evm_erc20_transfer) — both the source and destination are validated against the allowlist.
The address allowlist is enforced at the database level for on-chain executors. It is not a client-side check that can be bypassed.

Signed-Recipient Invariant (Validated == Signed)

The address allowlist answers “is this destination approved?” — but a separate question must also hold: does the transaction we are about to sign and broadcast actually pay that approved destination? A naive design validates one field (the intent’s destination) while signing a different, caller-controlled field — a “validated field != signed field” fund-outflow bypass. QTG closes this with a fail-closed, re-derive-from-the-artifact invariant on every on-chain send executor, at TWO chokepoints: (1) Prepare time — the recipient is resolved ONLY from the allowlist-validated intent destination (no caller-controlled override fallback) and asserted before signing; (2) Submit/broadcast time — the actual signed transaction is decoded (the signed legacy transaction is decoded from its raw bytes; for the Gateway EIP-712 lane the submitted burn-intent is re-hashed) and the recipient/target embedded in the bytes that go on-chain is re-derived and compared to the live approved destination (or, for source/contract-pinned actions, the trusted config value). A malicious or swapped signer, an injected signature result, or a tampered persisted prepared action therefore cannot broadcast a transaction that pays a different address than the approved destination — the executor fails closed before broadcast and the node terminalizes FAILED. When the check fails, one of these error codes is raised:
This invariant defends against a malicious or swapped signer, injected signature output, and prepared-action tampering. It does NOT defend against a full DB-row tamper that rewrites the trusted anchor and signed artifact together. QTG treats that as the DB-write threat boundary rather than an in-band approved-row-hash problem.

RBAC: Three Roles

Every API key is bound to exactly one role. The role determines what operations that key can perform: The Agent role is designed for LLM agents and automated systems. An agent key is bound to a specific authority that constrains:
  • Which templates the agent can use.
  • Which venues and assets the agent can target.
  • What volume the agent can propose.
Even if an agent key is leaked, the attacker can only propose movements within that authority binding — and those proposals still require operator approval (unless an auto-approve policy is in scope).

Audit Trail

Every state transition in QTG is recorded in an append-only audit_events table. This includes:
  • Movement creation, approval, rejection, and completion.
  • Node state transitions (dispatch, observe, fail, recover).
  • Signer rotation and retirement events.
  • Template and registry changes.
Audit events are queryable via the API. The primary endpoint is GET /v3/audit/events (admin + operator — and the read is itself recorded as a meta-audit row); the registry-scoped GET /v3/registry/audit is retained as a legacy surface.

Callback Security

When QTG notifies external systems about state changes, those callbacks are secured with HMAC v3 signatures:
  • Every callback includes a timestamp, nonce, and signature in the headers.
  • The signature covers the full request body, timestamp, and nonce — so tampering with any part invalidates the signature.
  • Nonce replay defense: receivers must store seen nonces and reject duplicates. This prevents an attacker from capturing and replaying a legitimate callback.
  • Callbacks are dispatched from a durable outbox — if delivery fails, QTG retries. The callback is not lost.
For the receiver-side implementation contract (what your callback endpoint must verify), see Callback Verification Contract. For a working example, see Callback Receiver Example.

Inbound Request Authentication

All API requests to QTG are authenticated using HMAC signatures:
  • Every request must include a key ID, timestamp, nonce, and signature.
  • The signature covers the HTTP method, path, query parameters, body hash, timestamp, and nonce.
  • Stale timestamps are rejected (clock skew tolerance is configurable).
  • Nonce reuse is rejected.
This means every API call is attributable to a specific key, and therefore a specific role. Combined with the audit trail, you have a complete chain: who requested what, who approved it, and what happened.

Summary: Defense in Depth

No single layer is the entire security story. The layers work together: For the full architecture overview, see Architecture Overview.