Auth Enable Rollout Runbook
Purpose: an operator checklist to close easy-to-miss preparation items in order before switching MG_AUTH_ENABLED=true
1. What this document covers
This document organizes the preparation and verification procedure required when enabling v3 inbound HMAC auth for the first time. Covered scope:- caller inventory cleanup
- role assignment
- client/key bootstrap with the auth seed CLI
- verification of caller-side HMAC signing implementation
- staging enable sequence
- smoke test
- rollback
- revoke/rotate lifecycle
- mTLS
2. Final target state
The final state should be the following.- All
/v3/**requests except/healthzrequire HMAC signatures - Only registered callers can send requests
- Each caller is assigned one of three roles (
admin/operator/agent) - Replay nonces are recorded/rejected
- Operators can trace which caller uses which key
2-1. Current verification state (2026-03-21)
The following has already been verified.- staging DB
qtg_v3created - five staging callers seeded
dashboard-staging→operatorops-cli-staging→operatoradmin-cli-staging→adminread-monitor-staging→operatorstrategy-engine-staging→operator(oragentif it only files template proposals)
- local real HTTP smoke test completed with
MG_AUTH_ENABLED=true/healthz→200- unsigned
GET /v3/signers→401 - operator key
GET /v3/signers→200 - operator key
POST /v3/movements→201 agentkey against anadmin-only route (e.g.PATCH /v3/executors/{executor_key}) →403- operator key
POST /v3/movements/{id}/approve→200 - admin key on an admin route -> expected
404after auth passes (route reached, target row absent)
.artifacts/auth-staging/staging-auth-seeds.json.artifacts/auth-staging/staging-auth-smoke-results.json.artifacts/auth-staging/staging-auth-external-http-smoke.json
- seed artifacts contain real secrets, so they must be treated as local secrets.
- Secret distribution to actual external caller processes (dashboard/ops-cli/strategy-engine) is not yet complete.
3. Preflight checklist
A. Caller inventory
First, list every system that actually calls/v3/**.
If even one caller is missing, 401/403 errors can occur immediately after enable.
B. Verify role mapping
The three roles are:admin— full access, including template / signer / executor mutationoperator— write and operate: create movements, approve/reject/retry/cancel, and all read surfacesagent— network-scoped automation: read surfaces plus its own agent-authority template proposals and bridge attempts (bound 1:1 to aAgentAuthority)
read role; read surfaces are reachable by operator (and by admin/agent
where the route allows it). The route → allowed-roles map lives in ROLE_ALLOWED_ROUTES
(src/qtg/interfaces/api/middleware/auth/hmac.py). Current meanings:
POST /v3/movements→ any role (admin/operator/agent)GET /v3/movements,GET /v3/movements/{id},GET /v3/movements/{id}/timeline→ any roleGET /v3/whoami,GET /v3/balances,GET /v3/route-catalog→ any roleGET /v3/plan-templates/{template_id}→ any rolePOST /v3/movements/{id}/approve|reject→admin/operatorPOST /v3/movements/{id}/actions/resume|retry|cancel→admin/operatorGET /v3/executors,GET /v3/signers→admin/operatorPOST /v3/plan-templates,PATCH /v3/plan-templates/{template_key}→adminPATCH /v3/executors/{executor_key}→adminPATCH /v3/signers/{signer_key}→adminPOST /v3/admin/cutover-mode→adminPOST /v3/registry/verify-drift→adminPOST /v3/agent-authorities/{authority_key}/template-proposals→agentPOST /v3/agent-authorities/{authority_key}/bridge-attempt→agent
Anagentkey is additionally namespace-scoped:AgentNamespaceMiddlewarerejects requests whose path does not match the key’s boundAgentAuthoritynamespace with403 agent_namespace_mismatch.
4. Key bootstrap
Seed CLI
Base command:--database-url— explicit target DB--hmac-secret— set the secret directly--cert-fingerprint— initial client metadata value
- Reuse the active client if one with the same name exists
- Otherwise create a new
ApiClient - Create a new
ApiClientKey - Generate a random secret if none is specified
- Print the result once to JSON stdout
Checks during bootstrap
- agree on the
client_namenaming rule (dashboard,ops-cli,strategy-engine, etc.) - agree on the
key_idnaming rule ({caller}-{env}recommended) - retrieve
hmac_secretfrom JSON stdout safely - prevent the secret from remaining in shell history / CI logs / shared paste
- place the secret into the actual caller’s secret store
5. Caller implementation checklist
Each caller must implement/verify the following.-
X-QTG-Key-Id -
X-QTG-Timestamp -
X-QTG-Nonce -
X-QTG-Signature - canonical string matches the server
PATHis the routed, percent-decoded path the server dispatches on — no origin, no query string.QUERYis the query exactly as sent: no leading?, no sorting, empty string if absent. Sorting before signing transmits bytes you did not sign, and every such request 401s.
- nonce is a new value for every request
- timestamp uses the current epoch seconds
- body hash is computed from raw bytes
- GET requests also require auth (except
/healthz)
6. Staging enable sequence
Phase 1 — verify wiring with auth off
-
MG_AUTH_ENABLED=false - deploy so that each caller can create signed requests
- prepare a smoke helper that can send signed requests in advance
Phase 2 — seed complete
- all required callers are seeded in the staging DB
- roles match the expected values
- secret distribution is complete for each caller
Phase 3 — smoke test
positive test:- operator key →
GET /v3/signers - operator key →
POST /v3/movements - operator key →
POST /v3/movements/{id}/approve - operator key ->
POST /v3/movements/{id}/actions/retryor/cancel - admin key ->
PATCH /v3/executors/{executor_key}orPATCH /v3/signers/{signer_key} - agent key →
POST /v3/agent-authorities/{authority_key}/template-proposals(within its namespace)
- unsigned request →
401 - wrong secret →
401 - stale timestamp →
401 - replay nonce →
401 - role not allowed for the route →
403 - agent key outside its bound namespace →
403(agent_namespace_mismatch)
Phase 4 — enable
-
MG_AUTH_ENABLED=truein the staging env - app restart / redeploy
- rerun the smoke test
7. Checks immediately after enable
-
/healthzis still unauthenticated - verify that unsigned
/v3/**requests are blocked - expected callers work normally
- auth failure reasons are logged
- verify
NonceRegistryentries are recorded
- a read-only monitor forgets GET auth
- an ad-hoc curl script is unsigned
- timestamp/nonce generation differs by caller, causing canonical mismatch
8. Rollback
If a problem occurs after auth enable, the simplest rollback is:MG_AUTH_ENABLED=false- app restart / redeploy
- Seeded
ApiClient/ApiClientKeyrows do not need to be deleted during rollback - After identifying the root cause, fix the signed caller side and retry
9. Completion criteria
If all of the following are satisfied, consider “auth enable preparation complete.”- caller inventory documented
- each caller role finalized
- key bootstrap completed with the seed CLI
- secret distribution completed
- staging positive/negative smoke tests passed
- rollback procedure verified
MG_AUTH_ENABLED=true be enabled.
10. Key Rotation Procedure
When a key must be replaced after auth enable (periodic rotation, suspected exposure, etc.).Principle
- Order: Seed new -> Deploy -> Verify -> Revoke old.
- If a new
key_idis issued for the sameclient_name, it can stay active at the same time as the old key. - Revoke the old key only after verifying that the caller works normally with the new key.
Procedure
Step 1. Check the current key
Step 2. Issue a new key
- Retrieve the
hmac_secretvalue from the output safely. - To avoid leaving it in shell history, use
history -dorHISTCONTROL=ignorespace.
Step 3. Deploy the new secret
Set the newkey_id and hmac_secret in the caller’s environment variables or .env file.
Step 4. Smoke test
Verify that normal requests succeed with the new key (see §6 Smoke test).Step 5. Revoke the old key
- Immediately after revoke, all requests using the old key are rejected with
401. - The DB row is not deleted and remains with
status=revokedfor auditability.
Step 6. Verify
- old key:
key_status=revoked - new key:
key_status=active
Emergency revoke (suspected exposure)
If exposure is suspected, run Step 5 immediately, then recover in order from Step 2. In this case the order is old-key revoke -> new-key issue -> deployment, so API access for that caller is interrupted from the revoke point until deployment of the new key is complete.11. Secret deployment model
Current model: env-based deployment
QTG uses a single-host Docker Compose + Tailscale access model. Secrets are injected per caller through each process’s environment variables.Secret protection rules
.envfiles usechmod 600and are included in.gitignore- The seed CLI output is shown only once, so save it immediately in a safe location
- Prevent secrets from remaining in CI/CD logs, shell history, or clipboard
- If later migrating to AWS Secrets Manager, Vault, and so on, keep env var names unchanged
Rate Limiting
After HMAC auth passes, an in-memory fixed-window rate check runs beforecall_next.
The limiter picks one of two per-key tiers from the caller’s role class (see
RateLimiter.check in src/qtg/infrastructure/security/rate_limit.py):
- write tier — keys whose role is
adminoroperator(the write/operate-class roles) - read tier —
agentkeys (read-class)
- The tier is selected by the key’s role class, not by the individual route. A higher write
limit therefore covers every route an
operatororadminkey can reach. - When the limit is exceeded:
429 {"detail": "rate limit exceeded"}+Retry-Afterheader /healthzand/dashboard/*are exempt from rate limiting- It does not operate when
rate_limit_enabled=Trueis set whileauth_enabled=False