Skip to main content

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 in create_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 into registry_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 with UndefinedColumn: source_ip and aborts before any key is persisted. (Verified on the 2026-05-15 dev migration.)

Steps

1. Apply the schema migration

Existing deployments use Base.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:
If alembic does not auto-load your .env, inject MG_DATABASE_URL explicitly 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:
Capture the JSON output. Store 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

Remove MG_DASHBOARD_WRITE_TOKEN from your .env:
If you skip this step, the server will refuse to start after the rebuild in step 4 (the fail-closed guard in 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_id and hmac_secret from 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_id string in localStorage, never the secret. The secret lives only in memory as a non-extractable CryptoKey for the duration of the browser session.

6. Smoke test

Note: in this release no dashboard HTTP route writes RegistryAuditEvent rows. The auth-log line emitted by require_dashboard_writer_hmac is the universal forensic surface for ALL dashboard write routes (movements, authorities, capital_transfers, routes POST, auto_approve). The registry_audit_events table’s new forensic columns (source_ip, user_agent, request_id) are reserved-for-future use when a dashboard route is added that writes RegistryAuditEvent. Today the only writer is the bootstrap CLI which leaves them NULL.
To 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):
  1. Perform the mutation from the dashboard and confirm the response is 200.
  2. Find the matching dashboard_writer_auth_ok log line:
  3. Verify the line contains request_id=<uuid>, key_id=<your operator key>, source_ip, user_agent, method, and path (with the concrete resource id, e.g., path=/dashboard/movements/<id>/approve).
  4. Cross-reference request_id with the X-Request-ID response header from the browser DevTools Network tab for the same request.

Revocation

If an operator leaves or a key is suspected compromised:
The next write attempt with that key returns 401. There is no grace period — revocation is immediate on the next request. To re-issue for the same operator, run the bootstrap CLI again with a new --key-id.