Skip to content

SCRUM-205 — admin-auth: bootstrap-root command

Plan ref: AA-6 (docs/11-admin-plane-plan.md). Stacked on SCRUM-204.

Behaviour

admin_auth bootstrap-root [-email root@otomo.internal] [-name Root] [-rotate] reads the root password from ADMIN_AUTH_ROOT_PASSWORD_FILE (default /run/secrets/admin_root_password; one trailing \n/\r\n trimmed; ≥16 chars; warns if group/world-readable; the password is never printed). Needs only ADMIN_AUTH_DATABASE_URL.

State Result
no root creates it (is_root, roles {admin}, created_by NULL), audit root.bootstrap → root created: <email>
root exists changes nothing → root already exists: <email> (+ a note if -email differs; the existing email is kept)
root exists, -rotate new hash, failures/lock reset, all root sessions revoked, audit root.rotate → root password rotated; N sessions revoked
no root, -rotate creates it (says nothing existed to rotate)
concurrent runs the single-root unique index decides; the losers report "already exists"

Root is the break-glass account (password-only, no TOTP); its job is creating personal admin accounts (SCRUM-206).

Test-infrastructure fix (from SCRUM-198): go test ./... runs packages in parallel, and store.TestMigrateUpDownUp drops the staff tables while the server and bootstrap packages use them — a latent flake. Every DB-backed test now holds a shared Postgres advisory lock for its duration.

How to verify

cd services/admin_auth
export ADMIN_AUTH_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/admin_auth_test?sslmode=disable'
go vet ./... && go test -race -count=2 ./...
go test -count=1 -v ./internal/bootstrap/
Test Proves
TestRunCreatesRoot row shape; password verifies with the file minus the newline; one root.bootstrap row
TestRunIsIdempotent second run: same hash, no new audit row
TestRunRotatesRoot new hash, old password rejected, root's sessions revoked, failures/lock reset, one root.rotate
TestRunRotateWithNoRootCreates as named
TestReadPasswordFileRules missing / empty / 15 chars → error that does not contain the file contents
TestRunConcurrentBootstrap 4 at once → exactly one root, every call succeeds
TestBootstrapRootCanLogIn the created root logs in through POST /admin-auth/login

CLI check done during review:

short password            -> "must be at least 16 characters", exit 1
0644 file                 -> warning, "root created: root@otomo.internal", exit 0
-email other@x            -> "root already exists … keeping the existing root email", exit 0
-rotate (new file)        -> "root password rotated; 0 sessions revoked", exit 0
audit                     -> root.bootstrap, root.rotate

Results at time of writing

  • go vet, go test -race -count=2 (7 packages) against Postgres 16: pass; the tests leave no root row behind.

How it was built

DeepSeek run scoped (Landlock) to services/admin_auth (313 s, ~47k output tokens). It also found and fixed the cross-package test race above. Claude review: logic read, suite run twice, CLI exercised. No changes needed.