Skip to content

feat(api): step-up authentication and in-band audit log on /admin/* writes (T7 part 1) #355

@ericfitz

Description

@ericfitz

Threat reference

T7 (Full-system compromise via admin-settings tampering) — see docs/THREAT_MODEL.md §4.

Problem

/admin/* system-settings endpoints today require only the admin role. There is no second factor and no per-write audit signal. An admin role obtained legitimately (insider) or via privilege escalation (T2) can rewrite OAuth/SAML provider config, disable the SSRF blocklist, weaken rate limits, or rotate the JWT signing key in a single API call with no friction and no per-event audit trail outside generic request logs.

Scope of this issue (re-scoped)

This is now Part 1 of a three-part T7 mitigation. Out-of-band alerting and dual-admin approval are tracked separately:

This issue covers only:

  1. Step-up authentication for any write to /admin/* via fresh-OAuth re-auth.
  2. In-band structured audit log entry on every /admin/* write.

Proposed fix

1. Step-up middleware (fresh-OAuth re-auth)

  • Add a RequireStepUp middleware that runs after AuthzMiddleware for routes flagged x-tmi-authz-step-up: required (vendor extension on the OpenAPI path).
  • The middleware checks the JWT for a recent auth_time claim (or equivalent fresh-auth marker). If now - auth_time > step_up_window (configurable; default 5 min), the middleware returns 401 Unauthorized with WWW-Authenticate: step-up realm="tmi", error="step_up_required" and a structured error body pointing to the re-auth flow.
  • The re-auth flow: TMI redirects the client through the user's primary IdP again (or surfaces a step-up endpoint that does so). On successful re-auth, a new JWT is minted with a fresh auth_time claim. The window resets.
  • This deliberately picks fresh-OAuth re-auth over TOTP/WebAuthn so the implementation reuses existing OAuth plumbing — no new authenticator registration, no new UX for first-time setup. The choice is documented; later issues can layer TOTP/WebAuthn on top if needed.

2. In-band structured audit log

  • Every /admin/* write emits an audit row at the existing audit_entries table (api/models/audit.go). The current schema scopes audit entries to a ThreatModelID; that column needs to become nullable (oracle-db-admin will need to review the migration) so system-level admin events can land here without a TM. Alternative: a parallel system_audit_entries table — to be decided in design.
  • The audit row carries: actor identity (already present fields), the field path (e.g., system_settings.oauth.providers.google.client_secret), the redacted old value (for secret-shaped fields), the redacted new value, and a stable change-summary string.
  • A registry of "secret-shaped" field patterns drives redaction (e.g., anything matching *.client_secret, *.signing_key, *.bearer_token).

3. OpenAPI vendor extension

  • Document the step-up requirement on each /admin/* write operation via x-tmi-authz-step-up: required.
  • Document the audit emission via a x-tmi-audit: { kind: admin_settings_change } extension so the audit handler is discoverable from the spec.

Acceptance criteria

  • A write to /admin/settings/{key} from a JWT older than the step-up window returns 401 with WWW-Authenticate: step-up. Integration test asserts header + status.
  • A successful step-up flow mints a new JWT and the same write succeeds. Integration test exercises the round-trip.
  • Every /admin/* write produces an audit_entries row with redacted old/new values for secret-shaped fields. Unit test asserts redaction; integration test asserts row presence.
  • Step-up middleware is gated by the x-tmi-authz-step-up vendor extension and is opt-in per route (so non-admin routes are unaffected).
  • DB-touching: the ThreatModelID-nullable migration (or separate system_audit_entries table) is reviewed by the oracle-db-admin subagent before merge.

Effort

M (re-scoped from the original M+ that included OOB alerting and dual-approval).

Related

Metadata

Metadata

Assignees

Labels

Projects

Status

Done

Relationships

None yet

Development

No branches or pull requests

Issue actions