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.
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_APPROVALwithout 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.
- Can this API key be restricted to specific IP addresses? Is that restriction mandatory or optional for withdrawal-enabled keys?
- Does the account support a withdrawal address whitelist, and can withdrawals be limited to whitelisted addresses only?
- Is the whitelist enforced per API key or per account, and is there a time delay before a newly added address becomes usable?
- 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?
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.lockpins exact versions; QTG additionally pins explicit security floors for known CVEs inpyproject.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 installis 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
Signcalls, 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:
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_KEYwith the plain backend. - The KMS key policy restricts
Signto 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_approle, 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
- How each control works → Security Model
- The verifiable control inventory and CI gates → Enterprise Readiness
- Applying the hardening artifacts → Deployment Hardening
- Configuring credentials and going live → 04 — Going Live