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:
- Step-up authentication for any write to
/admin/* via fresh-OAuth re-auth.
- 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
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:
/admin/*via fresh-OAuth re-auth./admin/*write.Proposed fix
1. Step-up middleware (fresh-OAuth re-auth)
RequireStepUpmiddleware that runs afterAuthzMiddlewarefor routes flaggedx-tmi-authz-step-up: required(vendor extension on the OpenAPI path).auth_timeclaim (or equivalent fresh-auth marker). Ifnow - auth_time > step_up_window(configurable; default 5 min), the middleware returns401 UnauthorizedwithWWW-Authenticate: step-up realm="tmi", error="step_up_required"and a structured error body pointing to the re-auth flow.auth_timeclaim. The window resets.2. In-band structured audit log
/admin/*write emits an audit row at the existingaudit_entriestable (api/models/audit.go). The current schema scopes audit entries to aThreatModelID; 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 parallelsystem_audit_entriestable — to be decided in design.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.*.client_secret,*.signing_key,*.bearer_token).3. OpenAPI vendor extension
/admin/*write operation viax-tmi-authz-step-up: required.x-tmi-audit: { kind: admin_settings_change }extension so the audit handler is discoverable from the spec.Acceptance criteria
/admin/settings/{key}from a JWT older than the step-up window returns401withWWW-Authenticate: step-up. Integration test asserts header + status./admin/*write produces anaudit_entriesrow with redacted old/new values for secret-shaped fields. Unit test asserts redaction; integration test asserts row presence.x-tmi-authz-step-upvendor extension and is opt-in per route (so non-admin routes are unaffected).ThreatModelID-nullable migration (or separatesystem_audit_entriestable) 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