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:
req.ip (or first hop of X-Forwarded-For when behind Fly's edge).
- 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.
- 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.
Context
Several routes accept the static
ADMIN_API_KEYas auth (synthetic user idadmin_api_key, set byserver/src/middleware/auth.ts:744-755). The most-exercised one isPOST /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'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
detailsfield with:req.ip(or first hop ofX-Forwarded-Forwhen behind Fly's edge).sha256(ADMIN_API_KEY).slice(0, 8)— so rotations show up as a fingerprint change in the log without exposing the key.X-Admin-Operatorheader echoed verbatim intodetails.operator. Internal scripts (the incident scripts inscripts/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
Files to touch
server/src/middleware/auth.ts— add fingerprint helper, attachreq.adminKeyFingerprintalongsidereq.isStaticAdminApiKey.isStaticAdminApiKey(grepisStaticAdminApiKey) — record fingerprint + IP + operator header in theirrecordAuditLogcalls.scripts/incidents/*— setX-Admin-Operatorso we know which script ran.