Migrating Dashboard Write Auth from Bearer (v1) to HMAC
This is a one-time migration when upgrading past the release that ships per-operator HMAC authentication for dashboard write routes.Pre-flight
Note: the new fail-closed guard is increate_app() only. CLIs (bootstrap_dashboard_writer_key,
alembic, seed scripts, etc.) can still import qtg.config.settings while
MG_DASHBOARD_WRITE_TOKEN is still present in your environment. The OLD app container can keep
running on the OLD code during steps 1–2; the cutover happens at step 4.
Ordering matters: Step 1 (apply the schema migration) MUST run before Step 2 (issue operator keys). The bootstrap CLI writes audit rows intoregistry_audit_events.source_ip / user_agent / request_id, columns that only exist after the migration. Running the CLI first on a pre-migration DB fails withUndefinedColumn: source_ipand aborts before any key is persisted. (Verified on the 2026-05-15 dev migration.)
Steps
1. Apply the schema migration
Existing deployments useBase.metadata.create_all() at server startup, which creates new
tables only — it does NOT add new columns to existing tables. The Alembic migration for this
release adds source_ip, user_agent, and request_id columns to registry_audit_events:
Ifalembicdoes not auto-load your.env, injectMG_DATABASE_URLexplicitly so the migration runs against the correct PostgreSQL database:
2. Issue a key for each operator who needs dashboard write access
On the host running QTG:key_id + hmac_secret in your password manager (a 1Password
vault entry saved under the dashboard origin URL is recommended — it enables browser autofill in
the Unlock modal).
Repeat for each operator. Each operator should have their own key so that revocation is
per-workstation, not fleet-wide.
3. Remove the legacy env var
RemoveMG_DASHBOARD_WRITE_TOKEN from your .env:
create_app() fires and emits a clear error pointing to this runbook).
4. Rebuild and restart
Rebuild both the API and dashboard images so the bundle drops the old build args and the backend picks up the new code:5. Unlock in browser
In each operator’s browser, open the dashboard. Click 🔒 Unlock writes.- Paste
key_idandhmac_secretfrom step 1. (1Password autofills if you saved a vault entry for the dashboard origin in step 1.) - Optionally check “Remember Key ID on this device” — this stores only the
key_idstring inlocalStorage, never the secret. The secret lives only in memory as a non-extractableCryptoKeyfor the duration of the browser session.
6. Smoke test
Note: in this release no dashboard HTTP route writesTo smoke-test forensics after a mutation (e.g., approve a pending test movement, or create a dashboard authority from the UI; both go through the same auth dependency):RegistryAuditEventrows. The auth-log line emitted byrequire_dashboard_writer_hmacis the universal forensic surface for ALL dashboard write routes (movements, authorities, capital_transfers, routes POST, auto_approve). Theregistry_audit_eventstable’s new forensic columns (source_ip,user_agent,request_id) are reserved-for-future use when a dashboard route is added that writesRegistryAuditEvent. Today the only writer is the bootstrap CLI which leaves them NULL.
- Perform the mutation from the dashboard and confirm the response is 200.
- Find the matching
dashboard_writer_auth_oklog line: - Verify the line contains
request_id=<uuid>,key_id=<your operator key>,source_ip,user_agent,method, andpath(with the concrete resource id, e.g.,path=/dashboard/movements/<id>/approve). - Cross-reference
request_idwith theX-Request-IDresponse header from the browser DevTools Network tab for the same request.
Revocation
If an operator leaves or a key is suspected compromised:--key-id.