> ## Documentation Index
> Fetch the complete documentation index at: https://jephalabs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Security Tradeoffs

> What QTG protects against, what it does not, and how your host environment changes the answer

# Security Tradeoffs

The [Security Model](/concepts/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

<Note>
  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.
</Note>

***

## 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.

| Attacker capability              | `MG_LOCAL_SIGNER_KEY` (plain, dev only) | 1Password-backed local signer | AWS KMS signer                            |
| -------------------------------- | --------------------------------------- | ----------------------------- | ----------------------------------------- |
| Reads files on disk              | **Key stolen**                          | Key not on disk               | Key not on disk                           |
| Dumps process memory             | **Key stolen**                          | **Key stolen**                | Key never in memory                       |
| Runs code inside the QTG process | **Key stolen**                          | **Key stolen**                | Can request signatures while access lasts |
| Steals your AWS credentials      | —                                       | —                             | Can request signatures while access lasts |

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.

<Warning>
  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.
</Warning>

***

## 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?

<Tip>
  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.
</Tip>

<Note>
  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.
</Note>

***

## 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:

| Surface                                | What it is                                                                 | Where malicious code runs                                               |
| -------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| **Python runtime dependencies**        | \~100 packages locked in `uv.lock`, imported by the QTG server and workers | **Inside the QTG process**                                              |
| **npm dependencies (dashboard)**       | The `frontend/` build, locked in `package-lock.json`                       | Your build machine, and the browser bundle — not the QTG server process |
| **Other packages on the same machine** | Anything else you `npm install` / `pip install` while developing           | Your user account — same filesystem as `.env`                           |

### 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:

|                                 | On-chain lane (KMS signer)                                                                          | CEX lane (exchange API)                                                                                                          |
| ------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Attacker inside the QTG process | Can request KMS signatures for arbitrary transactions. **Funds reachable by that key are exposed.** | Can call the exchange API with your credentials — but the **exchange-side** withdrawal whitelist and IP restriction still apply. |
| Surviving control               | KMS key policy, CloudTrail alerting, IAM revocation                                                 | Exchange whitelist + IP restriction                                                                                              |

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.

| Secret                   | An attacker with it **can**                                                                                       | **Cannot**                                                       |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| **Database credentials** | Everything. Read every secret stored in the DB, flip approval state, rewrite allowlist rows, alter audit history. | —                                                                |
| **Admin API key**        | Register templates, manage signers, change the registry and allowlist configuration.                              | Directly read the signing key material (it is in KMS).           |
| **Operator API key**     | Approve movements, retry failures, trigger capital transfers.                                                     | Register templates or manage signers.                            |
| **Agent API key**        | Propose movements **within its authority binding** (templates, venues, assets, volume).                           | Approve anything. Escalate outside the binding.                  |
| **Callback HMAC secret** | Forge state-change notifications to your receiver.                                                                | Move funds — callbacks are outbound notifications, not commands. |
| **Dashboard writer key** | Add or revoke allowlist addresses through the dashboard write tier.                                               | Approve movements (that is the operator role).                   |

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](/security/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](/security/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.

|                                 | Shared dev laptop                                                                 | Dedicated machine or VM             | Cloud VM + IAM role                       |
| ------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------- |
| Plaintext secrets on disk       | `.env` present                                                                    | `.env` present                      | None — instance role supplies credentials |
| Supply-chain sweeper exposure   | **High** — you run `npm install` / `pip install` on the same filesystem as `.env` | Low — no unrelated package installs | Low                                       |
| AWS credential form             | Long-lived key file (`~/.aws/credentials`)                                        | Long-lived key file                 | **Temporary, rotated automatically**      |
| Blast radius of one bad package | Secrets, AWS credentials, browser sessions, SSH keys — all at once                | QTG secrets only                    | QTG secrets only                          |
| Recommended for                 | Evaluation and development                                                        | Real funds, with care               | Real funds — default recommendation       |

The message underneath the table is simpler than the table:

<Warning>
  **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.
</Warning>

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](/security/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](/guide/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](/concepts/security-model)
* **The verifiable control inventory and CI gates** → [Enterprise Readiness](/security/enterprise-readiness)
* **Applying the hardening artifacts** → [Deployment Hardening](/security/deployment-hardening)
* **Configuring credentials and going live** → [04 — Going Live](/quickstart/04-going-live)
