Skip to content

Audit-log enrichment for static ADMIN_API_KEY paths #4208

@bokelley

Description

@bokelley

Context

Several routes accept the static ADMIN_API_KEY as auth (synthetic user id admin_api_key, set by server/src/middleware/auth.ts:744-755). The most-exercised one is POST /api/organizations/:orgId/members/by-email (server/src/routes/organizations.ts:3008), used by internal tooling and incident scripts (e.g. scripts/incidents/2026-04-triton-promote-hayem.ts).

When the static admin key invokes one of these routes, audit log entries record:

  • workos_user_id: 'admin_api_key'
  • inviter_email: 'admin-api-key@internal'
  • the route + verb

That tells us the static key was used but not which operator or script used it. If the key ever leaks, we can't trace which client owned the breach.

Proposal

For every static-admin-gated mutation, enrich the audit details field with:

  1. req.ip (or first hop of X-Forwarded-For when behind Fly's edge).
  2. A non-reversible fingerprint of the key — sha256(ADMIN_API_KEY).slice(0, 8) — so rotations show up as a fingerprint change in the log without exposing the key.
  3. An optional operator-supplied X-Admin-Operator header echoed verbatim into details.operator. Internal scripts (the incident scripts in scripts/incidents/) already know who's running them; teach them to set the header.

This is a cross-cutting hardening — affects every static-admin-gated route, not just by-email. Surfaced by security-reviewer in PR #4207.

Out of scope

  • Rotating or revoking the existing key (separate ops task).
  • Changing the auth model to require human-in-the-loop (rejected during the same review — break-glass scripts need the static path).

Files to touch

  • server/src/middleware/auth.ts — add fingerprint helper, attach req.adminKeyFingerprint alongside req.isStaticAdminApiKey.
  • All routes that gate on isStaticAdminApiKey (grep isStaticAdminApiKey) — record fingerprint + IP + operator header in their recordAuditLog calls.
  • scripts/incidents/* — set X-Admin-Operator so we know which script ran.

Metadata

Metadata

Assignees

No one assigned

    Labels

    admin-toolInternal tools for AAO staffclaude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.governanceIssue concerns the governance protocol domain

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions