Skip to main content

Security Tradeoffs

The Security Model page describes what QTG defends. This page describes the other half: what it does not defend, and why. QTG protects your funds from your own automation and your own mistakes — a runaway strategy, a buggy rebalancing script, an LLM agent that proposes the wrong transfer, a fat-fingered amount. That is the threat it was built for, and it is good at it. QTG is not a custody product. It does not protect you from an attacker who owns the machine it runs on, and it does not protect you from code running inside its own process. Those are real threats, they are not exotic, and this page tells you exactly where the line is so you can decide what to run where.

The one-sentence version

Almost every QTG guard lives inside the QTG process and its database. Anything that runs inside that process — including one of its own Python dependencies — is inside those guards. The controls that survive a fully compromised host are the ones enforced somewhere else: your exchange, and your cloud provider’s key policy.

1. Signing keys

QTG’s default EVM signer is AWS KMS in your own AWS account. This is the strongest position in the codebase, and it is worth understanding precisely what it buys. The important distinction in the last two rows: KMS protects the key material, not the signing capability. An attacker with code execution in your QTG process cannot exfiltrate a key they can reuse forever from anywhere — but they can ask KMS to sign, for as long as they hold that access. Every such request is recorded in CloudTrail, and revoking the IAM principal ends it immediately. That is a materially better position than a stolen private key, but it is not immunity.
The plain environment-variable signer (MG_LOCAL_SIGNER_KEY with backend plain) puts a raw private key on disk and in process memory. It exists for development. Do not point it at real funds.

2. Exchange credentials

Exchange API credentials are a different shape of problem, and QTG cannot solve it the way it solves signing keys. Exchange authentication is an HMAC bearer secret (Upbit’s JWT, Binance/Bybit/OKX query signatures, and so on). To sign a request, the secret has to be plaintext in process memory. There is no equivalent of “the key never leaves the HSM” that QTG can apply for you today — the secret is issued by the exchange, and QTG must present a derivation of it on every call. By default, qtg init writes these credentials to your .env file. They are plaintext on disk. So the defense is not “an attacker cannot steal this credential.” The defense is “a stolen credential cannot move your funds.” That is a weaker-sounding claim, but it is the one that actually holds — and it is layered: What QTG enforces in code (verifiable in this repository):
  • Exchange whitelist preflight. Before submitting any withdrawal, the CEX executor fetches your address book from the exchange and refuses to proceed unless the destination is on it (ADDRESS_NOT_WHITELISTED). This runs on every withdrawal, every time.
  • QTG’s own address allowlist, checked at dispatch, with an optional registration cooling period.
  • The approval gate — nothing dispatches from PENDING_APPROVAL without an approval action.
  • Outflow velocity cap on CEX balance lanes — a rolling-window ceiling that brakes a flood of individually-valid withdrawals.
  • Approval-pinned destinations. A CEX withdrawal address must be a literal in the approved, hash-pinned plan. A runtime-supplied address that diverges is rejected.
What your exchange enforces (outside QTG, and therefore outside a host compromise): This is the layer that survives when everything on your machine is lost. Exchange capabilities vary and change, so verify these for each venue you use rather than assuming:
  1. Can this API key be restricted to specific IP addresses? Is that restriction mandatory or optional for withdrawal-enabled keys?
  2. Does the account support a withdrawal address whitelist, and can withdrawals be limited to whitelisted addresses only?
  3. Is the whitelist enforced per API key or per account, and is there a time delay before a newly added address becomes usable?
  4. Can you issue a key with withdrawal scope disabled for the read-only work (balances, deposit history) and keep withdrawal scope on a separate, tightly restricted key?
If your venue supports both IP restriction and a withdrawal address whitelist, a leaked .env does not by itself let an attacker move funds off that exchange: they are not calling from your IP, and they cannot withdraw to an address you never whitelisted. Configure both before you deposit real capital.
Some exchanges’ HMAC schemes could in principle be backed by a managed HMAC key (for example AWS KMS GenerateMac), keeping the secret out of the process entirely. QTG does not implement this today. Treat exchange credentials as plaintext-in-memory secrets.

3. Supply chain: your dependencies run inside the guards

This is the threat most self-hosted deployments underestimate, and it deserves its own section because it is the one case where QTG’s internal guards do not help at all. There are two distinct dependency surfaces, and they are not equally dangerous:

Why a poisoned Python dependency is the worst case

Every guard described in the Security Model — the approval gate, the address allowlist, RBAC, the outflow cap — is code and data inside the QTG process and its database. A malicious package in QTG’s own import graph is on the inside of all of it. It can read settings, open the same database session, flip an approval row, and call the signer directly. Stating it plainly: a compromised runtime dependency defeats the approval gate. No in-process control can defend against code running in the same process. What is left is the asymmetry between lanes: This is the strongest practical argument for configuring your exchange-side protections: they are the only controls in the entire stack that keep working after the host is fully owned.

What actually reduces this risk

  • Keep the lockfile authoritative. uv.lock pins exact versions; QTG additionally pins explicit security floors for known CVEs in pyproject.toml. Refresh dependencies deliberately, not incidentally.
  • Review dependency bumps like code. A version bump on a package in the import graph of a fund-moving service is a privileged change.
  • Do not install unrelated packages into the same environment. A tutorial’s pip install is an import-graph change.
  • Do not build the dashboard on the machine that runs the server. npm postinstall scripts are the classic supply-chain vector, and they are a build-time surface — keep it off the runtime host.
  • Constrain the signer at the cloud boundary. A KMS key policy that only a specific IAM role can use, plus CloudTrail alerting on Sign calls, turns an in-process compromise from silent into noisy and revocable.
  • Separate the machine. See the next section — this is the single highest-leverage change.

4. Other secrets — what a leak actually costs

Signing keys and exchange credentials are not the only secrets QTG holds. This table is what each one buys an attacker. Two things worth internalizing from this table: The agent role is the one designed to leak. It is the key you hand to an LLM agent or a strategy bot, and its blast radius is deliberately bounded by an authority binding plus the approval gate. That is the intended failure mode. Database write access is the accepted boundary. QTG states this explicitly: an adversary with DB write is game over, and the project does not claim otherwise. The controls in Deployment Hardening — a least-privilege qtg_app role that cannot execute DDL, separated from the migration owner — shrink the blast radius and raise the effort floor. They do not remove the boundary. See Enterprise Readiness for the full statement.

5. Where you run it matters

The same QTG deployment has meaningfully different security depending on the host. This is usually a bigger lever than any configuration change. The message underneath the table is simpler than the table:
Separate the machine that installs packages from the machine that moves money.Supply-chain attacks land where development happens. If your trading host never runs npm install, never opens a browser, and never clones an unfamiliar repository, most of this threat class does not reach it — regardless of whether that host is EC2, a spare laptop, or a VM on the machine under your desk.
A cloud VM with an IAM instance role has one additional structural advantage worth calling out: there is no credential file on disk at all. The bootstrap problem — “where do I keep the secret that unlocks the other secrets?” — simply does not arise. A local host running against AWS always needs long-lived credentials somewhere, or short-lived SSO credentials that expire and need human refresh.

6. Checklist before real funds

  • EVM signing uses AWS KMS, not MG_LOCAL_SIGNER_KEY with the plain backend.
  • The KMS key policy restricts Sign to the QTG principal only, and CloudTrail alerting is on.
  • Every exchange API key has IP restriction configured where the venue supports it.
  • Every exchange account has a withdrawal address whitelist, and it contains only addresses you intend to use.
  • Read-only work uses a separate key without withdrawal scope.
  • QTG runs on a host that does not build software or install unrelated packages.
  • The database uses the least-privilege qtg_app role, not the migration owner. See Deployment Hardening.
  • The approval gate is intact — auto-approve policies, if any, are scoped by template and strategy with budget limits.
  • The outflow velocity cap is configured for the tokens you move.
  • Address allowlist entries are chain-scoped, and the registration cooling period is enabled.

What this page does not cover