RUNBOOK.md 3.4 KB

Admin Bootstrap Operator Runbook

How to create the first administrator of a fresh d-oauth install, and how to replace that bootstrap account immediately. There is no HTTP bootstrap endpoint: provisioning is CLI/Job only.

Task 48 owns SECURITY.md; this file is the task-47 operator runbook. The threat model and the replace-immediately rationale live there.

Prerequisites

  • Migrations applied exactly once via the migration Job (never per replica): d-oauth-server migrate
  • Zero enabled admins exist (a migrated database seeds zero users).

1. Bootstrap the first admin

Interactive (password read from stdin, never argv, never logged):

d-oauth-server admin bootstrap --email ops@example.com

Non-interactive / cluster Job (mounted secret file):

d-oauth-server admin bootstrap --email ops@example.com \
  --password-file /run/secrets/bootstrap-password

When neither a file nor stdin yields a password, the command generates a 256-bit password and prints it exactly once, and only to a TTY. If stdout is not a TTY the command refuses instead of risking the secret in Job logs: re-run attached to a terminal or supply --password-file.

A second admin bootstrap refuses while any enabled admin exists:

d-oauth-server admin bootstrap --email second@example.com
# error: already exists: an administrator already exists (1); ...

The override requires explicit --force, prints a loud warning to stderr, and emits an audited admin.bootstrap.force event. Prefer routine provisioning instead:

d-oauth-server admin bootstrap --email second@example.com --force
d-oauth-server user create --admin --email named-admin@example.com

2. Replace the bootstrap admin immediately

The bootstrap account starts with must_change_password=true and totp_enabled=false. Its first login yields a restricted session that can only change the password and enroll TOTP — /authorize, /oauth/*, and admin-gated actions deny it until both steps complete.

  • Log in as the bootstrap admin; you land on /login/change-password.
  • Change the password (minimum 12 characters). This clears must_change_password and stamps password_changed_at.
  • Enroll TOTP at /login/totp/setup and confirm with an authenticator code. The session is re-issued with full authority only now.
  • Capture the recovery codes shown once at enrollment; store them offline.
  • Create a named second admin: d-oauth-server user create --admin --email <name>@example.com
  • Log in as the named admin and verify full access.
  • Disable the bootstrap account: d-oauth-server user disable --email ops@example.com
  • Rotate/expire the bootstrap secret: delete the mounted /run/secrets/bootstrap-password material and revoke any copy of a generated one-time password.

3. Audit events

On the audit target (never carrying secrets): admin.bootstrap.created and admin.count.changed on success, admin.bootstrap.refused on refusal, admin.bootstrap.force on a forced override. First-login, password-changed, and TOTP-enrolled outcomes are emitted by the login flow itself.

4. What never happens

  • No default or hardcoded admin credential ships; migrate seeds zero users and serve never creates one.
  • The bootstrap password is never accepted via argv, never logged, and never stored (only its Argon2id hash reaches Postgres).
  • Bootstrap is CLI/Job only; no HTTP endpoint can create an admin.