LocalPrivateKeySignerEvm — Operator Reference
Tier-3 signer. Plaintext key material is held in process memory. This is the correct choice for local evaluation and development. For production fund movement, migrate to AWS KMS (Tier 0) or a Pro Tier-1 signer (Vault Transit, GCP KMS) before increasing volume.Source of truth:
src/qtg/infrastructure/signers/local_private_key_evm.py
1. When to use / when not to use
Use when
- Evaluating QTG before committing AWS infrastructure.
- Running a development or CI environment where AWS KMS is unavailable.
- Non-AWS deployments (GCP, Azure, bare-metal) that have not yet provisioned a Tier-1 signer.
- Single operator / solo quant experimentation at low fund-movement volume.
Do not use when
- Fund movement volume is high or consequence of key compromise is severe.
- You require a cryptographic sign-without-expose guarantee (key material never leaves the HSM or KMS).
- You are running on Windows (use WSL2, which gives you a Linux environment).
- Compliance requirements mandate hardware-backed key storage (YubiHSM 2, HSM-backed KMS).
Security posture summary
2. Quickstart
Backend A: 1Password CLI (op) — recommended Tier-3
- Install the 1Password CLI.
- Store your EVM private key (32-byte hex, with or without
0xprefix) as a password field in a 1Password item. Note theop://reference URI:op://<vault>/<item>/<field>. - Set env vars:
- Sign in to 1Password CLI before starting QTG:
REFETCH_PER_SIGN=true (default) re-invokes op read on every sign() call.
The working key buffer is wiped immediately after signing. The op subprocess adds
~200–500 ms per signing call, which is acceptable for CCTP/Gateway workloads of 1–2
operations per minute.
Backend B: Plain env var (plain) — quickstart / dev only
This backend emits a startup warning and should never be used in production.
plain with MG_WORKERS_ENABLED=true (necessary for dispatch/observe
workers), add the explicit opt-in:
ValueError at startup when both key_source=plain
and workers_enabled=true are set. This prevents accidental production deployment with
plaintext keys in a fully-live worker context.
3. systemd unit example (Linux)
Place the following fragment in/etc/systemd/system/qtg.service. The hardening
directives block core dumps and kernel module loading at the OS level, complementing
the in-process prctl(PR_SET_DUMPABLE=0) + setrlimit(RLIMIT_CORE=0) that QTG
applies at bootstrap when this signer is active.
Operator note onSystemCallFilter:@system-servicepermitsfork/execveso subprocess calls to theopbinary work. If you tighten beyond this set, verify that the chosen key-source backend’s subprocess invocation is not blocked. Test withjournalctl -u qtg | grep -i seccompafter deploy.
Operator note onPR_SET_DUMPABLE: Withprctl(PR_SET_DUMPABLE=0)in effect, the QTG process is non-traceable bygdb/straceeven from the same UID. Operators who rely on attaching debuggers for live diagnosis should use AWS KMS or a Pro Tier-1 signer instead. Theprctlcall is scoped to thelocal_private_key_evmbootstrap branch only; AWS KMS bootstrap paths are unaffected.
4. macOS launchd plist example
Place at~/Library/LaunchAgents/com.jephalabs.qtg.plist (user-space) or
/Library/LaunchDaemons/com.jephalabs.qtg.plist (system-space for server installs).
5. Memory hygiene + Python str-on-heap limitations
What QTG does
- The 32-byte private key working copy is stored as a
bytearray(mutable, zeroable). - The buffer is zeroed immediately after signing completes when
REFETCH_PER_SIGN=true. Whenrefetch_per_sign=True, a stack-local bytearray is used per sign call — the buffer never touchesself._key_buffer, making concurrentsign()calls safe. __repr__and__str__return a masked string (key=REDACTED). The_key_bufferfield is excluded from the dataclass auto-repr (repr=False).- The
opbinary is resolved to an absolute path at signer construction viashutil.which("op"). The absolute path (e.g./usr/local/bin/op) is used asargv[0]in every subsequentsubprocess.runcall, preventing PATH-mutation TOCTOU attacks. - To pin a specific binary explicitly, set
MG_LOCAL_SIGNER_OP_PATH=/absolute/path/to/op(must be an absolute path to an existing executable). The signer rejects relative paths and non-existent paths at construction withFatalMovementError(SIGNER_REJECTED). An empty or unset value falls back toshutil.which("op")onPATH. - The
opsubprocess uses bytes mode (text=False) so the raw output goes directly into the bytearray without Pythonstrinterning. - The signer module never logs the key buffer, signature components (
r,s), or raw env var values.key_refis logged only assha256(key_ref)[:16]fingerprint.
Process hygiene: fail-closed startup behavior
prctl(PR_SET_DUMPABLE=0) (Linux) and setrlimit(RLIMIT_CORE, 0) (Linux + macOS) are
applied at bootstrap before the key is loaded. If these fail, QTG blocks startup by
default rather than starting in a silently degraded state.
Override (not recommended for production):
signer.health() under the "hygiene" key:
all_critical_applied is true only when every platform-applicable hardening succeeds.
On non-Linux: only rlimit_core must be "applied". On Linux: both must be "applied".
Acknowledged limitations (Python cannot overcome these)
No global log redaction filter is installed. A regex
^[0-9a-fA-F]{64}$ would
also redact valid 32-byte audit data (CCTP burn txids, signed tx hashes, keccak digests)
which QTG intentionally logs. Defense stays at the signer-module boundary.
6. Health output schema
GET /v3/signers/{signer_key}/health (or the batch health endpoint) returns:
7. Migration path to AWS KMS or Pro Tier-1
Workers must be paused during the cutover. Switching the signer while in-progress nodes hold references to the old signer can produce signatures from the wrong address (allowlist drift, double-spend windows in worst case).-
Provision the target backend out-of-band (AWS KMS key + IAM role, Vault Transit
mount + token, GCP KMS keyring + service account, etc.). Derive the new EVM address
from the new backend (call
bootstrap_signer_address()on a throwaway init or use the health endpoint after a test deploy). - Add the new signer address to any on-chain allowlists (CCTP attester allowlist, Stargate trusted bridge address, etc.) while keeping the old address active. Both addresses authorized simultaneously bridges the cutover.
-
Pause workers: set
MG_WORKERS_ENABLED=false, restart, wait for in-flight nodes to reach a terminal or dispatchable boundary. Monitornode.stateuntil no node is inSUBMITTINGorPREPAREDfor the affected signer. -
Switch
MG_LOCAL_SIGNER_BACKENDand supporting env vars, restart QTG with workers still disabled. -
Verify: the
health()endpoint for the new signer returns the expectedsigner_address. Run a dry-run movement (no actual broadcast) end-to-end. - Send a small testnet transaction through a real movement on testnet to confirm the proving lane works against the new backend.
-
Re-enable workers (
MG_WORKERS_ENABLED=true, restart). Watch the first 1–2 movements closely. - Drain the old proving wallet to the new address (safe now — no live signing happens with the old key).
- Remove the old address from on-chain allowlists (revoke).
-
Wipe old key material: remove the 1Password entry, or
unset MG_LOCAL_SIGNER_PRIVATE_KEY_HEXand remove it from.env.
8. Why no Windows support
LocalPrivateKeySignerEvm relies on:
prctl(PR_SET_DUMPABLE, 0)— Linux-only syscall (no equivalent in Win32 API usable from pure Python without a C extension).resource.setrlimit(RLIMIT_CORE, (0, 0))— POSIX only (available on Linux + macOS; not available in CPython on Windows).systemdunit hardening directives — Linux-only.- The
opCLI and 1Password desktop app integration path is tested on Linux + macOS; Windows agent behavior is outside this spec’s scope.
AWS-strict tooling caveat
Local private-key signer has no live-readiness preflight in v1. The following operator tools remain AWS-KMS-only and are intentionally unchanged in this release. They will return errors or skip whenMG_LOCAL_SIGNER_BACKEND=local_private_key_evm
is active:
This is a deliberate scope boundary. Touching these tools would expand blast radius
beyond the agreed Tier-3 scope (each tool would need its own loader + tests +
signer-construction branch). A capability-based preflight refactor is planned for the
first Pro Tier-1 signer release (Vault Transit / GCP KMS).
To validate before mainnet use: sign a known unsigned testnet transaction through a
real movement (template + approval + actual broadcast on testnet) and confirm the
recovered address + on-chain inclusion. Operators who need a richer programmatic
preflight should defer mainnet adoption until either (a) they migrate to AWS KMS where
the existing preflights cover them, or (b) the Pro Tier-1 sign-without-expose signers
ship with a capability-based preflight refactor.