Devboxes
Feature Catalog

Security Audit Log

The append-only trail of security-relevant actions and the shared trust boundary for audit and session client IPs.

Every security-relevant action writes one append-only audit row: who acted (a user, or the system itself), the affected organization and subject, the client IP and user agent when they are trustworthy, and a few non-secret facts about the action. Platform administrators — the same platform-administrator role described in Organizations, roles & access — read the trail on the Audit log screen under Identity, filterable by action, organization, and date range, newest first, paged by cursor. Rows expire after 180 days.

Set API_TRUSTED_PROXY=true only when the API's direct peer is the Devboxes edge proxy and that proxy overwrites x-forwarded-for. Otherwise a direct client can forge the IP metadata stored with audit events and sessions.

The wired action vocabulary doubles as the filter facet on the screen and the accepted values of the API's action filter:

ActionsWritten when
auth.gate_violation, auth.user_created, bootstrap.operator_createdAccount creation is accepted, rejected at the invite gate, or claims the first platform-administrator role
credential.created, credential.updated, credential.deletedAn organization credential or repository connection changes
github_app_authorization.revokedGitHub revokes the App authorization
invitation.created, invitation.accepted, invitation.resent, invitation.revokedAn organization invitation changes
machine.registered, machine.deletedA runner machine registers, revives, or is deleted
member.role_changed, member.removed, org.renamed, org.deletedOrganization access or identity changes
repo_connection.restored, repo_connection.revoked, repo_connection.suspendedA remembered GitHub App installation changes availability
run.dispatchedA run is dispatched, in the same transaction as the run itself
session.revoked, task_token.rejectedA session is revoked or a task token fails authorization
user.email_change_requested, user.email_change_confirmed, user.password_changedAccount credentials change
user.disabled, user.enabled, user.deletedPlatform access is disabled, restored, or erased

How to test it

SuiteProves
pnpm vp run @firops/api#testaudit-log.integration.test.tsAdministrator-only access; machine.registered, credential.created, credential.deleted, run.dispatched, and auth.user_created land their row with actor, subject, and first-hop IP; a rolled-back transaction leaves no phantom row; key material never lands in a row; keyset paging, filters, and the 180-day sweep
pnpm vp run @firops/api#testdeletion-receipt.integration.test.tsAn erased account and an erased organization each leave a user.deleted / org.deleted receipt carrying its pre-erasure counts, pseudonymized so it outlives the subject it records
pnpm vp run @firops/api#testtwo-factor.integration.test.tsA second factor is stored only after a real code confirms it, and neither the shared secret nor the backup codes are kept in the form the client received
pnpm vp run @firops/api#testaudit-log.untrusted-proxy.integration.test.tsWithout a declared trusted edge, a forged forwarding header is ignored and the row records no client IP
pnpm vp run @firops/api#testsession-ip.integration.test.tsWith a declared trusted edge, a Better Auth session records the first x-forwarded-for hop
pnpm vp run @firops/api#testsession-ip.untrusted-proxy.integration.test.tsWithout a declared trusted edge, Better Auth ignores forged x-forwarded-for and x-real-ip values and records no session client IP
pnpm vp run @firops/api#testsignup-gate.integration.test.tsAn authorization revoked during account creation rolls back the user and records auth.gate_violation without exposing invitation material

Manual smoke: sign in as a platform administrator → Identity → Audit log; dispatch a run and watch its row appear; apply an action filter and step through pages with Next/Previous.

Decisions

  • No foreign keys — audit rows must survive deletion of the user, organization, or subject they describe. A trail that disappears with its subject is not a trail.
  • Audit rows commit in the mutation's own transaction — a rollback leaves no phantom row claiming something happened, and a committed mutation cannot lose its row to a later failure.
  • Rows expire after 180 days — a bounded forensic window instead of unbounded retention of user IPs. The sweep rides the existing periodic mechanism: once at boot to catch downtime, then daily.
  • A forwarded client IP is believed only when the deployment declares its edge — with API_TRUSTED_PROXY=true, audit rows and Better Auth session rows record the first x-forwarded-for hop because the edge proxy overwrites any client-supplied value. Unset (the default), forwarding headers are attacker-controlled input on a direct connection, so audit rows record no IP and Better Auth disables session IP tracking rather than persisting a forgeable value; its IP-keyed sign-in rate limiting is also disabled. This posture is for local development only.
  • Metadata carries small non-secret facts only — provider ids, names, model ids; never tokens or key material. The credential test serializes the whole row and asserts the key is absent.
  • Keyset pagination, with an accepted precision limit — pages follow a (time, id) cursor so new rows cannot shift pages mid-read. The cursor carries millisecond precision while rows store microseconds, so rows that share the page-boundary row's millisecond but differ below it can be skipped when stepping to the next page. Accepted deliberately as a known limitation instead of complicating the cursor.
  • The sole declared action without a live writer is run.cancelled — run cancellation remains outside the audit trail until every terminalization path can commit the same idempotent event. Sign-in failure bursts are skipped rather than building bespoke auth middleware.

Terms of ServicePrivacy Policy

On this page