KMS Signer Rotation Runbook
Scope: Scheduled rotation of a production EVMAwsKmsEvmSigner key.
Covers preflight → drill → cut-over → KMS key deletion → exception handling.
See the Pro signer lifecycle reference for
the lifecycle API and CLI contract.
This runbook covers prod EVM AwsKmsEvmSigner only.
For Callback HMAC v3, CEX API keys, or Tier-3 local signer rotation, see
§ 7 Related rotation procedures.
1. Overview & Control Objective Mapping
Control objective:KMS asymmetric key rotation is a scheduled operator-run procedure. The procedure, actor, asset movement, and completion time leave an evidence chain in the audit log and rotation snapshot.
Control limit: this covers app-role compromise protection. It does not cover a broad AWS account compromise or IAM root takeover.
Evidence chain quick-look query
2. Preflight
2-1. Resolve the current signer identity (required — Gate 3 backstop)
The Gate 3 nonterminal-node scan also matchessigner_binding['signer_address_snapshot'].
Record old_address before rotation. Use the supported synchronous bootstrap
method; do not adapt an older async probe:
bootstrap_signer_address() is synchronous. Record only the resulting address
in the operator note, never KMS credentials or provider response payloads.
2-2. Create a new KMS asymmetric key
KeyMetadata.KeyId and KeyMetadata.Arn.
Minimum IAM policy (grant to the QTG runtime role)
2-3. Confirm the new signer EVM address
candidate_live_address. It is the fresh live identity
used by the defense-in-depth verification below.
2-4. Enroll the replacement through runtime bootstrap
Add anaws_kms_evm replacement entry to MG_EXTRA_SIGNERS_JSON using the
same signer_key, kms_key_id, aws_region, and optional aws_profile /
aws_endpoint_url fields used by the runtime bootstrap. Treat the JSON value as
secret-bearing configuration: do not paste it into tickets, logs, transcripts,
or the operator evidence packet.
Restart the Pro application. Startup registers the configured signer and runs
sync_registry_to_db() to materialize its registry row. Do not create a
replacement runtime enrollment with direct SQL: a database row alone neither
registers the in-memory signer nor proves its KMS identity.
2-5. Verify roster enrollment and pin expected identity
Keep workers enabled withMG_WORKERS_ENABLED=true and wait for the
executor-health worker to complete a successful signer health refresh. That
worker persists the EVM observed_signer_address; a roster GET alone does not
materialize the observed identity.
Before pinning, confirm GET /v3/signers includes <NEW_SIGNER_KEY> as
active with a fresh observed identity and the deliberately unpinned state:
identity_state: "missing_expected", STOP and fix enrollment before the
expected-identity request. observed_signer_address is diagnostic and
non-authoritative; it is not a substitute for the server’s fresh rotation-time
comparison.
Pin the independently recorded candidate identity with the shared signed HTTP
client. QTG_KEY_ID, QTG_HMAC_SECRET_FILE (or QTG_HMAC_SECRET), and
QTG_API_URL must already be configured; do not print any of their values.
QtgHttpClient.request("PUT", ...) provides the HMAC-signed transport; there
is no dedicated expected-identity CLI command.
GET /v3/signers after the successful PUT. Require the pinned
expected_signer_address, the fresh observed_signer_address, and
identity_state: "match". Any absence, invalid address, or mismatch is a
STOP; do not proceed to drill or cut-over.
2-6. Defense-in-depth identity verification (required)
Rotation-time identity enforcement runs inside the Pro server. For every--drill request and every live --ack-impact rotation, the server automatically resolves the replacement signer’s fresh runtime identity and compares it with the persisted expected_signer_address. The CLI sends the rotation request; it does not perform this comparison locally.
The server fails closed before impact calculation or any audit event, rotation event, metadata, or authority-remap write. observed_signer_address is diagnostic and non-authoritative; identity_state: "match" remains useful roster and health-refresh evidence but cannot authorize rotation. Identity denial returns HTTP 409 with one stable code and no address or backend-cause detail:
Keep the independent KMS probe from Step 2-3 and compare its result with the
pinned expected value before drill. This manual probe remains a defense-in-depth check; missing, invalid, or mismatched values are a STOP even though the server repeats the fresh resolution and comparison for the request.
2-7. Check new signer health (drill mode)
outcome: "drilled" in the output JSON.
3. Run Drill Mode (recommended before cut-over)
Impact report review checklist
Drill mode does not change signer bindings. Record the audit/event identifiers
and the impact report in the sanitized operator evidence.
4. Live Cut-over
Follow the 9-step sequence. Complete each step before moving to the next.Step 1 — Re-resolve replacement identity immediately before rotation
Do not reusecandidate_live_address from the earlier drill preparation.
Immediately before the state-changing command, resolve the replacement KMS
identity again and compare it with the pinned value from the latest roster GET:
expected_signer_address and pre_cutover_live_address match;
this is a second independent resolution, not a reuse of the drill probe. It is
defense in depth: the server performs its own fresh resolution and comparison
again when it handles the live rotation request.
From this fresh probe through completion of the rotation response, STOP if
any operator or deployment action would repoint or mutate the replacement KMS
alias/key or replace the runtime signer object. Do not continue the in-flight
request; restore a stable configuration, repeat the fresh probe, and submit a
new rotation request.
The automatic gate validates one fresh identity snapshot plus continuity of
that same runtime signer object for the request; it does not perform a second
in-lock KMS identity resolution. Keep the replacement configuration stable
until the rotation response completes.
Step 2 — Rotate (deprecate old, remap authorities)
old_signer_key.rotation_metadata.deprecated_from is set.
The OLD key is then excluded from new movement and template-compile candidates.
In-flight nodes can continue to sign, observe, and recover with the OLD key. This is expected.
Step 3 — Capture pre-drain inventory
Step 4 — Manual drain (per chain)
For each chain, use this order: (1) provision gas -> (2) move ERC-20 assets -> (3) move native dust last. Use a QTG movement or direct calldata. A retire attempt before drain completion is blocked by Gate 2.Step 5 — Scan allowances
approve(0) calldata.
Step 6 — Manual revoke
Revoke each non-zero allowance from Step 5 in the protocol UI. Record revoke txids.Step 7 — Recheck post-drain inventory
Step 8 — Recheck allowances
--ack-allowances
only when there is a reason revoke cannot be completed, and record that reason in the operator note.
Step 9 — Retire after a zero-allowance scan
--ack-allowances only when non-zero allowances deliberately remain and
the operator has recorded why they cannot be revoked; then use the exceptional
acknowledged-residual command:
Unit warning forAll four gates must pass for retire to succeed:--dust-threshold-usdThis parameter is not USD; it is token units in the smallest denomination. The Phase 7 v1 implementation compares raw balances without real USD conversion. In production, conservatively drain to actual zero before retiring.1.0means “1 token unit” (for example, 0.000001 USDC for USDC).
There is no
--force flag. If a gate fails, see § 6 Exception scenarios.
5. Delete the AWS KMS Key (post cut-over, out of band)
This section is outside QTG. Run it after QTG retire completes.-
Remove the
kms:Sign/kms:GetPublicKeypolicy for the OLD key through the IAM console or CLI. -
Schedule deletion. It can be canceled with
cancel-key-deletionduring the waiting period:
6. Exception scenarios
Drain does not complete
In-flight node breaks
Deprecated signers can still sign, observe, and recover in-flight nodes — deprecation stops new assignments, it does not revoke an in-flight one. If a node actually breaks, check the recovery worker logs and then run:Follow-up checks after drill failure
Drill failure means no state mutation occurred. Check whether the new signer passeshealth(),
whether signer_registry.status = 'active', and whether signer_key has a typo.
Gate 2 dust threshold exceeded
Gate 3 finds nonterminal nodes
- Wait: wait until the node reaches a terminal state (recommended).
- Recover: use the supported cancel, retry, or resume action as appropriate, then retry retire only after the node is terminal.
Gate 4 allowance ack incomplete
After revoke completes, retry retire without--ack-allowances.
Proceed with --ack-allowances only when there is a reason UI revoke cannot be completed,
and record the incomplete-revoke reason in the operator note.
7. Related rotation procedures
This runbook covers prod EVMAwsKmsEvmSigneronly. The Pro KMS Signing Gateway (post-GA Pro spec), including runtime app-role compromise protection, is handled by a separate spec. The AWS KMS rotation row indocs/runbooks/secret-rotation-checklist.mdforward-references this runbook from the secret inventory table.
8. SOC2/ISO27001 Evidence Query
8-1. All rotation actions in the last 12 months
event_type: rotate / rotate_drill / inventory / allowance_scan /
retire / retire_denied. retire_denied is reserved for retirement hard-gate
denial evidence. Identity and health rotation denials do not create an event.
8-2. Actor role attribution
Useactor_client_id + actor_key_id to look up the executor’s role:
RBAC v2 alembic 0003 swapped thepurposecolumn torole. Environments migrated from the old schema may haveack.roleas NULL.
8-3. Query the full snapshot for a specific signer
sre.snapshot: drill/rotate -> impact report JSON, inventory/retire -> balance snapshot.
References
- Pro runtime CLI:
qtg signer - Signer protocol and the local AWS KMS signer:
reference/signers/signer-protocol.md - Control objectives for this rotation: section 1 of this runbook