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: dashboard mutations DO produce durable audit rows today — they go through audit_log_post_commit -> the audit outbox -> promoted by the audit_outbox_promoter worker into the network-scoped audit_events table with the forensic columns (source_ip, user_agent, request_id, method, path) populated from request state. (The table was renamed from registry_audit_events to audit_events in the squashed 0000b OSS baseline — historically migration 0004 of the pre-squash chain; the RegistryAuditEvent symbol alias remains for the bootstrap CLI writer.) The auth-log line emitted by require_dashboard_writer_hmac remains the immediate forensic surface for ALL dashboard write routes (movements, authorities, capital_transfers, routes POST, auto_approve) — cross-reference both when investigating.
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, use the governed revoke (never raw SQL — a key ID and DB URL are not authenticated authority), with an active HMAC-admin credential:
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.