Bootstrap and human authentication flow
Overview
Fresh native-IAM bootstrap creates human and service administrators, shares their Role through separate bindings, and commits them with the Installation. It writes the initial service key to protected storage. Production also delivers a generated human password; development uses its configured password. This flow covers initialization, human sign-in, and exact IAM authorization. The service API key flow covers verification, rotation, and revocation.
Entry Points
- Trigger:
node scripts/bootstrap-installation.mjswithNODE_ENV=developmentorproduction,POST /api/auth/sign-in/email,POST /api/auth/providers/github/start,GET /api/auth/providers/github/callback, the matchinggoogleroutes, or a protected controller request. - Source:
scripts/bootstrap-installation.mjs,apps/controller/src/auth/index.ts:createControllerAuth, andapps/controller/src/index.ts:createFastifyApp. - Assumptions: Migrated PostgreSQL, configured Better Auth, native IAM bootstrap, protected output storage, disabled public signup, and exact IAM authorization. Production receives a private password path and sibling service-key path; development receives only the service-key output path.
Flow
graph TD
subgraph Bootstrap["Shared installation initializer: one attempt"]
A["Load Installation"] -->|Existing| B["Verify persisted identity; retain credentials"]
A -->|Fresh| C["Create human and native IAM seed with service administrator"]
C --> D["Better Auth persists service-key hash"]
D --> E["Sync private key JSON and production password file"]
E --> F["Commit Installation, IAM seed, and audit"]
end
F --> G["Complete startup"]
B --> G
Bootstrap -->|Any error| H["Exit unsuccessfully; preserve tracked artifacts for manual repair"]
subgraph Request["Human controller request"]
G --> P["Verify password or enrolled GitHub identity"]
P -->|GitHub profile enabled| Q["State rechecks account and method; commits session and audit"]
Q --> J["Release session cookie"]
P -->|Password-only profile| J
Q -->|Disabled, stale proof, or commit failure| R["Reject login; no cookie"]
J --> N{"Unsafe session request?"}
N -->|Yes| O["Check console origin and Fetch Metadata"]
N -->|No| K["Resolve current IAM identity and exact authority"]
O -->|Trusted| K
O -->|Rejected| M
K -->|Allowed| L["Run and audit OCC operation"]
K -->|Invalid session or denied authority| M["Return 401 or 403"]
endExecution Trace
1. Load state and create the fresh administrator identities
scripts/bootstrap-installation.mjs
loads the singleton Installation. Existing Installations only verify the
configured administrator's immutable account/IAM identity: no key issuance,
output changes, or identity/grant repair, including Installations predating service-administrator bootstrap.
For fresh setup, production creates a Better Auth account with a random password;
development creates the configured OPENCLAW_DEV_EMAIL/OPENCLAW_DEV_PASSWORD
account. packages/iam/src/index.ts:createBootstrapAdministratorSeed adds a
non-Agent spn_<uuid> without a Namespace and a separate unrestricted binding to
the human's administrator Role. The authorization reference
defines exact actions. Additional-account provisioning creates no service administrator.
2. Issue private output, then commit the Installation
apps/controller/src/auth/index.ts:createServiceKey persists a Better
Auth key named bootstrap-admin with the default 30-day expiry, scoped to this
Installation and service principal. This auth write is independent of the OCC
transaction. An uncommitted IAM seed cannot authorize normal OCC operations;
startup does not expose the application until bootstrap succeeds.
bootstrap-output.ts:writeProtectedBootstrapFile
creates owner-only output exclusively and syncs it before OCC commit. The JSON
contains the key response and attempt Installation ID; production also writes its
password file on the same protected PVC. Development writes to its bootstrap-only
volume or explicit direct-initialization path. No plaintext reaches logs, audit, HTTP
bootstrap responses, or the worker.
Both modes commit Installation/IAM/audit through the same controller
transaction. The initializer owns one attempt scope for account creation, key
issuance, output, and commit. The API subsequently loads committed state without
signing into itself or calling POST /installation/bootstrap; that public
endpoint remains human-session-only and does not issue bootstrap credentials.
Singleton database constraints select at most one committed seed. A losing
initializer fails and preserves completed tracked artifacts for operator
inspection.
Any error ends the single initialization attempt with
installation.bootstrap-failed, available non-secret IDs and paths, and a
nonzero exit. Completed tracked accounts, keys, and files remain available for
manual inspection; even a partially written output file is preserved. One
pre-return Better Auth failure is narrower: if password linkAccount fails
inside createAccount, the helper attempts to delete the just-created user
before rethrowing. The initializer does not treat that cleanup attempt as a
general artifact-recovery path, and it does not automatically revoke, retry,
repair, or reset committed or uncertain state.
The Helm initialization Job uses backoffLimit: 0.
The operator confirms the original transaction has finished and compares exact attempt IDs before manual repair; file existence or another Installation is insufficient. An uncertain commit can already have persisted the seed, so an error never authorizes an automatic wipe. A deliberate reset must identify the disposable Installation and its dedicated storage. The recovery procedure owns those operator actions.
After confirmed success, the operator retrieves/imports the existing file and retains its non-secret IDs. Lost output does not trigger regeneration; normal service-key management owns replacement and revocation.
3. Construct session authentication
apps/controller/src/auth/index.ts:createPostgresControllerAuth uses OCC's
public binding: the caller-owned pool and
full schema, no construction I/O or teardown, rejection without fallback, and
unchanged PostgreSQL/camelCase/transaction settings.
apps/controller/src/auth/index.ts:createControllerAuth configures Better Auth
email/password authentication, protected session cookies, and durable PostgreSQL
storage. Sign-in returns { authenticated: true }; the session token stays
in its HttpOnly cookie and is omitted from session-inspection responses.
safeSessionResponse projects sessionKey, an HMAC of the session record ID
under the auth secret (apps/controller/src/auth/session-binding.ts), alongside
public user identity. Console compares it to invalidate retained views and drafts
after a new session, including for the same user. Sign-out revokes the session,
and public signup is disabled. Without an external provider,
auth/admission.ts:passwordFailureAdmission limits failed password sign-ins.
requireSessionKey applies the optional x-occ-session-key header after the
cookie session resolves, in ControllerAdmissionVerifier.verify (protected API
and native admin proxy), session, resolveSession, and signOut. An absent
header changes nothing; a malformed, duplicated, or foreign key returns 401, so
the header narrows but never selects a session. Sign-out with a foreign key
revokes and clears nothing. The native admin proxy strips the header upstream.
When GitHub is configured, apps/controller/src/auth/github.ts:createHumanLogin
wraps the Better Auth adapter and provides curated password, GitHub, and logout
endpoints. packages/occ/src/state/human-authentication.ts:PostgresHumanAuthentication
owns persisted account/method checks and the original State transaction.
Password verification captures the credential and account version before the
session transaction rechecks them. Both methods pass a controller-private proof
to the same guarded session creation path; the session and required audit commit
before Better Auth releases its cookie. Session reads check the current account,
method, version, and Principal, with an eight-hour absolute lifetime and no refresh.
HTTPS uses a __Host- session cookie so a sibling host cannot plant the active
cookie through a parent-domain Domain attribute. Session readers and logout
reject ambiguous duplicate active-session cookies.
For GitHub, the Console reads GET /api/auth/providers and sends a same-origin
POST /api/auth/providers/github/start. The server stores a five-minute attempt with state and browser
secret digests, provider instance, callback, and PKCE verifier. A host-only
HttpOnly cookie binds the browser; this profile rejects shared-domain sessions.
Authorization requests omit OAuth scopes. Callback consumption commits before
exchange; a losing, expired, or invalid attempt does not exchange a code.
apps/controller/src/auth/github.ts:exchangeGithubSubject exchanges the code
with the GitHub App client ID and secret, uses the returned user access token
only for /user, and returns the numeric subject. Access and refresh tokens,
expiry, and scope data are discarded; the App private key remains with the
repository credential consumer. The subject selects an exact existing enrollment;
email, login name, and tokens do not become identity or policy. Success redirects
to exactly /console/; failure redirects to the fixed
Console URL with a sanitized error marker.
Start also returns attemptId, an HMAC of the attempt's state digest. Success sets
a signed two-minute SameSite=Strict receipt naming the new session and that
attemptId. The Console's same-origin POST /api/auth/providers/github/result
reaches oceGithubResult, which checks the receipt signature and expiry, the
posted attemptId, and that the session cookie still resolves to the named
session. It then records the receipt in a process-local ledger until expiry,
clears the cookie, and returns the session key, without issuing or extending a
session. Password sign-in in this profile returns the same key. Callback denials are audited as
INVALID_ATTEMPT (malformed, unbound, replayed, or expired), PROVIDER_UNAVAILABLE
(transport failure, deadline, 429/5xx, malformed body), or EXTERNAL_IDENTITY_REJECTED;
State dependency failure or uncertain session completion is not a denial. Neither path retries.
Google reuses apps/controller/src/auth/github.ts:externalProviderEndpoints for
start, callback, and result, with provider instance google:<sha256(client ID)>.
Authorization adds scope openid email and an auth-secret HMAC of attempt state
as nonce, without extra storage.
apps/controller/src/auth/google.ts:exchangeGoogleSubject exchanges the code, fetches
Google's signing keys through the same bounded transport, verifies the RS256 ID token's
signature, issuer, audience, expiry, and nonce (plus hd and email_verified when
allowed domains are set), and returns only sub. Tokens and email are discarded.
Password and external-provider work have separate bounded process-local admission; GitHub and Google share one budget. Provider HTTP shares a deadline and limits streamed response bytes; State bounds pending attempts and expired cleanup. State sets the five-minute attempt and eight-hour session deadlines. Cookie Max-Age subtracts monotonic elapsed work from that persisted lifetime; expired completion cannot release a cookie.
Activation requires stopped admission, drained or terminated requests, and every
old controller stopped. Both PostgreSQL compositions reject
GitHub with enabled native administration, even when its cookie domain is missing.
apps/controller/src/auth/index.ts:createPostgresControllerAuth constructs and
initializes authentication before activation, checking the secret, canonical HTTP
origin, and supported profile. Invalid static configuration leaves legacy sessions,
account enrollment, and the recovery designation unchanged.
PostgresHumanAuthentication.activateRecovery then validates and enrolls the complete existing password-user/Principal population,
fixes the usable recovery administrator, and removes unbound historical sessions
in one State transaction before serving resumes. Unsupported or incomplete
populations fail activation. This is a stopped-maintenance contract; startup does
not fence an old live reader. See the
deployment procedure.
The controller's account routes authorize native IAM Installation administer
and require a current human session and the configured Origin. State locks both
actor and target, rechecks the actor session, and applies the caller's
expectedVersion. Attachment, disablement, and account-wide revocation advance
that version and invalidate target sessions and proofs without changing IAM.
A guarded read returns current account and method state, not a prior operation
receipt. Unknown completion returns an explicit dependency failure without
replay or compensation; operators must resolve uncertainty before a new action.
Logout commits deletion and audit before clearing the cookie. The authentication reference
owns configuration, recovery limits, and operator-visible behavior.
4. Admit and authorize protected API calls
ControllerAdmissionVerifier.verifyControllerRequest requires the configured console Origin for
unsafe session requests before admission. A supplied Sec-Fetch-Site must be
same-origin. Sign-out applies the same check before revoking the session, and the
GitHub result exchange before reading it; that exchange shares the GitHub admission lane.
Explicit service API keys do not use the cookie origin check, and an invalid key
cannot fall back to a cookie.
apps/controller/src/index.ts:createFastifyApp validates the session, resolves
its installation-owned issuer and user ID through the selected IAM Driver, and
authorizes the exact resource through that same Driver. The Driver loads current
policy separately for identity lookup and authorization, so account and
permission changes are visible across controller instances. Missing or invalid
sessions return 401; denied permissions return 403; dependency failures fail
closed. Bearer credentials and caller-supplied identity headers are rejected.
5. Provision additional accounts
apps/controller/src/index.ts:createFastifyApp permits an authorized human
Installation administrator to create another account, with or without GitHub
sign-in. prepareAccount validates and hashes the password without writing; the
PostgreSQL composition then calls provisionPasswordAccount, which writes the
user, password method, Principal, explicit existing-role binding, enrollment,
and audit in one State transaction, so a failure leaves no partial account.
Nothing is compensated after the transaction. A lost COMMIT reply returns 503
stating that the outcome is unknown; the account is either complete or absent,
so a deliberate retry with the same email creates it only if the first attempt
did not commit, and otherwise returns 409.
Account creation issues no session and infers no grants.
Debugging and Verification
node --test tests/integration/native-admin-access.test.mjscovers trusted and untrusted origins on session mutations and sign-out, plus service-key admission.node --test tests/integration/postgres-production-wireup.test.mjswithOCC_PRODUCTION_WIREUP_DATABASE_URLproves actual bootstrap, protected random password/key delivery, human sign-in, service-key access, and no reissue on rerun.node --test tests/integration/postgres-auth-accounts.test.mjswithOCC_TEST_DATABASE_URLcovers account provisioning, transactional rollback, and a lost provisioning COMMIT reply. Its fresh development bootstrap case additionally verifies the service identity, protected output, and key access; it skips when an Installation already exists.node --test tests/integration/bootstrap-output.test.mjscovers exclusive output and rejected unsafe paths. Failed writes retain any created file. Database cases require the disposable PostgreSQL setup; an unconfigured/skipped suite is not runtime proof.node --test tests/integration/postgres-bootstrap-failures.test.mjswithOCC_BOOTSTRAP_FAILURE_DATABASE_URLexercises concurrent production attempts and preserves both environment modes' credentials when a test fault discards the acknowledgement after a real COMMIT. The suite resets a dedicated loopback database; see its settings.- Verify copied output is
0600without printing it; use a key-authenticatedGET /installationand Namespace create/read to check current authority. A401indicates credential rejection;403indicates identity/scope/policy denial. Preserve failed bootstrap artifacts and compare safe IDs through operator recovery. pnpm typecheck,pnpm format:check, andpnpm check:workspacevalidate source and workspace structure. Compose/PVC permission checks require real runtime execution; chart rendering alone does not prove storage access.
