fix(gateway): serialize bootstrap setup with an advisory lock (#1430) #1431

Open
fred wants to merge 1 commits from fix/bootstrap-race into next
Collaborator

What

POST /api/bootstrap/setup checked the zero-user count and then created the first admin with nothing making the two atomic (TOCTOU race). Two concurrent setup requests could both observe zero users and each create an admin user plus an admin API token.

Setup now runs inside db.transaction that first takes pg_advisory_xact_lock(BOOTSTRAP_SETUP_LOCK_KEY). The second concurrent caller blocks on the lock until the first transaction commits, re-reads the count, and gets 403. The lock is transaction-scoped and lives in the database, so it serializes across gateway replicas as well as within one process.

BetterAuth's createUser still writes on its own connection (unchanged); the verification reads moved onto the transaction and see the committed row under READ COMMITTED.

Tests

  • bootstrap.e2e.spec.ts: mock db gained transaction/execute; the 201 path now asserts the advisory lock is taken before the zero-user count; new suite pins 403 + createUser never called when users already exist. 5/5 pass.
  • pnpm typecheck (gateway): clean.

Provenance

webui-audit mission, A3 security review finding (cross-review confirmed), S1 quick-fix list in the lane REPORT.md.

Closes #1430

## What `POST /api/bootstrap/setup` checked the zero-user count and then created the first admin with nothing making the two atomic (TOCTOU race). Two concurrent setup requests could both observe zero users and each create an admin user plus an admin API token. Setup now runs inside `db.transaction` that first takes `pg_advisory_xact_lock(BOOTSTRAP_SETUP_LOCK_KEY)`. The second concurrent caller blocks on the lock until the first transaction commits, re-reads the count, and gets 403. The lock is transaction-scoped and lives in the database, so it serializes across gateway replicas as well as within one process. BetterAuth's `createUser` still writes on its own connection (unchanged); the verification reads moved onto the transaction and see the committed row under READ COMMITTED. ## Tests - `bootstrap.e2e.spec.ts`: mock db gained `transaction`/`execute`; the 201 path now asserts the advisory lock is taken **before** the zero-user count; new suite pins 403 + `createUser` never called when users already exist. 5/5 pass. - `pnpm typecheck` (gateway): clean. ## Provenance webui-audit mission, A3 security review finding (cross-review confirmed), S1 quick-fix list in the lane REPORT.md. Closes #1430
fred added 1 commit 2026-08-26 22:24:54 +00:00
POST /api/bootstrap/setup checked the zero-user count and then created the
first admin with nothing making the two atomic, so two concurrent setup
requests could both pass the check and each create an admin user plus an
admin API token.

Setup now runs inside a db.transaction that first takes
pg_advisory_xact_lock(BOOTSTRAP_SETUP_LOCK_KEY): the second concurrent
caller blocks until the first commits, re-reads the count, and gets 403.
The lock is transaction-scoped, so it also serializes across gateway
replicas sharing the database.

Spec: mock db gained transaction/execute; new assertions pin
lock-before-count ordering, and a new suite pins 403 + no createUser call
when users already exist.

Closes #1430
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Required
Details
This pull request doesn't have enough required approvals yet. 0 of 1 official approvals granted.
You are not authorized to merge this pull request.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/bootstrap-race:fix/bootstrap-race
git checkout fix/bootstrap-race
Sign in to join this conversation.