Scribe can use one scribe-auth service for several independent storage
servers. Auth owns users, credentials, server identities, and grants. Storage
servers own repositories and enforce permissions through AuthorizedRemote,
a transport-neutral RemoteOps implementation. No repository contents or
existence checks are sent to auth.
Authenticated storage connections use protocol 3 over TLS 1.3. Existing TCP2 operation encodings and all existing object/history formats are unchanged. An authenticated listener never negotiates plaintext or an older protocol. Auth itself uses the separate binary SAU1 protocol over TLS 1.3. There is no HTTP dependency, JWT signing, or custom cryptography. Server connections use Tokio and tokio-rustls; storage operations retain immediate redb durability.
Credentials and roles
Every user has an immutable random 128-bit ID, an immutable username, enabled status, and a separate auth-administrator flag. Regular users log in with a username and password to obtain an opaque, revocable session. Passwords use Argon2id v19 (19,456 KiB, two iterations, one lane, 32-byte output) with a fresh 16-byte OS-random salt and a server-only 32-byte pepper. The result is stored as a PHC string. Passwords contain 12–1024 UTF-8 bytes; there is no whitespace normalization. Password files may end with one LF or CRLF, which is removed when read.
Tokens contain 256 bits of OS
randomness; only their full BLAKE3 digests are stored at auth. Tokens are
individually revocable and expire. Disabling a user disables all their tokens.
The initial admin identity is reserved and cannot be disabled; its bootstrap
token can be revoked after another administrator/token is established.
Usernames can be short names (1–64 ASCII letters, digits, ., _, -)
or email-style/AD user principal names such as alice.smith@company.example
and alice+build@company.example (up to 254 ASCII bytes). Principals have
exactly one @ with nonempty parts; the local part allows letters, digits
and .!#$%&'*+-=?^_ plus backtick, braces, pipe and tilde. The suffix allows
letters, digits, ., _, and -. Whitespace, control characters, path
separators and quoted addresses are rejected. Names retain their exact
spelling and are case-sensitive; use the same spelling in all commands.
This accepts a UPN as a Scribe identifier; it does not connect to AD or verify
a mailbox. No storage or protocol migration is needed.
With the management connection options configured as below:
auth users add alice.smith@company.example
auth users password alice.smith@company.example --password-file ./alice.password
auth grants set alice.smith@company.example studio/REPOSITORY_ID --role writerEach registered storage-server identity belongs to exactly one namespace. Give each server its own revocable service token, even when several servers share a namespace. Service tokens can validate users and read decisions only for that namespace. They cannot list users, issue tokens, or edit grants.
Repository keys are strings: <namespace>/<immutable-repository-id>.
For example studio/6f76a25d5d0840c299c23771657b5b34. The server derives this
mapping, never the client. Display-name changes do not change permissions.
Auth accepts grants for any well-formed string before a repository exists;
there is no repository registry in auth. Two servers hosting independent
repositories named game have different IDs and independent permissions.
Restoring a backup preserves the repository identity and its grants; it is
recovery of that repository, not a new independent clone.
| Role | Allowed |
|---|---|
| Reader | Repository discovery and metadata, history, object presence/download, workspace registration, branch and merge reads |
| Writer | Reader plus object uploads, submits, branch create/rename, own workspace locks |
| Maintainer | Writer plus generation-checked forced unlock |
| Namespace administrator | All repository roles in that namespace, plus repository creation |
| Auth administrator | Central user/token/server/grant administration; no implicit storage access |
Granting namespace administration is an explicit, separately audited command. Repository maintainers do not gain permission to manage central users/grants. Access without a matching grant is denied. Repository discovery filters out inaccessible repositories. Operation outcomes are visible only to their recorded user, who must still have repository read access.
Setup
Build all three binaries with cargo build --release --locked.
Provision PEM certificates with scribe-auth certs or scribe-server certs;
both use the same offline helper. An existing certificate authority can also
provide them. Certificates must have SANs matching the hostname or IP clients
use. Do not use the checked-in test certificates. Restrict credential/key directories to
the service account; new credential files are mode 0600 on Unix and inherit
the directory ACL on Windows. Secrets are read from files, not command-line
arguments, and are not stored in .scribe/ workspaces or logged.
Generate a private CA and server certificates
Install OpenSSL 3 or newer on the provisioning machine. Scribe invokes it
directly without a shell; it is not needed to run the services. Use
certs --openssl /absolute/path/to/openssl ... to select a trusted executable
(on macOS the system openssl may be LibreSSL; use OpenSSL 3 instead).
Create the CA once, then issue separate keys/certificates for auth and storage:
scribe-auth certs init-ca --out ./studio-ca
scribe-auth certs issue --ca ./studio-ca --out ./auth-tls \
--dns localhost --ip 127.0.0.1
scribe-server certs issue --ca ./studio-ca --out ./storage-tls \
--dns localhost --ip 127.0.0.1Replace localhost with the real DNS name clients connect to. Repeat --dns
and --ip for additional identities, up to 64 total. DNS names must be exact
ASCII hostnames (use punycode for international names); wildcards and URLs are
not accepted. IPv4 and IPv6 addresses belong in --ip.
init-ca produces ca.pem and ca-key.pem. Each issue directory contains
server.pem, server-key.pem, and a copy of ca.pem. Keys use ECDSA P-256;
the CA can sign leaf certificates only, and issued certificates permit server
authentication only. The helper verifies the certificate chain and key pair
before reporting success. It does not create users or tokens.
Every output directory must be new, with an existing parent. Existing
directories, files, and symlinks are refused; there is no overwrite flag.
Unix directories are created with mode 0700 and files with mode 0600. On
Windows, first restrict the parent directory's ACL to the operator/service
account; generated files inherit it. Private keys are unencrypted so services
can start unattended. Keep studio-ca/ca-key.pem offline and backed up; give
each server only its own leaf key/certificate and the public CA certificate.
Normal scribe clients authenticate with their username and password and save their own session. The
server sends its public CA during TLS setup: keep the generated ca.pem
beside server.pem, or supply a complete certificate chain in the configured
certificate PEM. Neither private key is sent. Generation also prints the
full CA fingerprint for administrators to share with users.
CA lifetime defaults to 3650 days; server certificates default to 365 days.
--days accepts 1–3650, and the issuer must remain valid for the requested
server lifetime. For renewal, rerun issue with the same CA and a new
output directory, then configure the service to use the new files and restart
it. Existing clients keep trusting the same CA. Replacing the CA itself
requires explicit approval of the new trust on each client. These certificates
are a private studio PKI, not publicly trusted certificates.
If generation fails, the new directory may contain incomplete output. The
helper reports failure and retains it for inspection; do not deploy that
directory. Correct the cause and use a new output directory. No repository or
auth data root is read or changed by certs.
In the setup below, use auth-tls/server.pem / auth-tls/server-key.pem in
place of auth.pem / auth-key.pem, and the corresponding storage-tls files
for storage. Use studio-ca/ca.pem wherever ca.pem is shown.
Start services and provision users
Initialize auth once. The administrator token output belongs outside the new empty auth data directory:
scribe-auth init --data-dir ./auth-data --admin-token-out ./admin.token \
--pepper-out ./auth.pepper
scribe-auth serve --data-dir ./auth-data --pepper-file ./auth.pepper --listen 127.0.0.1:7443 \
--tls-cert ./auth.pem --tls-key ./auth-key.pemAuth uses async connection tasks and a bounded blocking storage pool. serve
accepts --max-connections (default 128) and --storage-workers (default 16,
range 1–1024). TLS handshakes and incomplete frames do not occupy storage
workers. I/O and worker admission have a 10-second timeout. Shutdown closes
idle connections; after 30 seconds it interrupts remaining network I/O and
waits for any started storage jobs to finish before releasing the root lock.
Auth uses the same scribe-log backend as the storage server. It logs to
stderr by default; for a long-running service, enable daily files:
scribe-auth serve --data-dir ./auth-data --pepper-file ./auth.pepper --listen 127.0.0.1:7443 \
--tls-cert ./auth.pem --tls-key ./auth-key.pem \
--log-dir ./auth-logs --log-format json --log-keep-files 14Files are named scribe-auth.YYYY-MM-DD.log. Rotation happens on the first
write after midnight UTC without a restart. Retention defaults to 14 files;
--log-keep-files must be at least 1 and affects only the auth log prefix.
File logs have no terminal colors. Without --log-dir, text stderr logs
automatically use terminal colors and respect NO_COLOR and TERM=dumb.
All auth commands accept --log-level, --log-filter, --log-format text|json,
-v (debug), -vv (trace), and -q (errors only). Explicit logging flags take
precedence over SCRIBE_LOG; the default level is info. Conflicting options
and invalid filters are rejected before a command mutates auth state. Command
results remain on stdout; --log-format json changes diagnostics only.
Lifecycle events include auth.listening (with the bound listen address),
auth.stopped, and file-logged startup/runtime failures as auth.failed.
Existing auth library diagnostics also reach this subscriber. Logs do not
include tokens or raw request contents. The bounded background writer drains
on graceful shutdown; a stalled sink can drop diagnostics, counted by the
shared logging backend, and forced termination can lose buffered events.
These operational logs are separate from durable auth state and auditing.
The management commands below use a shell helper to keep the examples short. Run them in another terminal while auth is running:
auth() {
scribe-auth --auth 127.0.0.1:7443 --auth-ca ./ca.pem \
--admin-token-file ./admin.token "$@"
}
auth users add alice
auth users add bob
auth users list
auth users password alice --password-file ./alice.password
auth users password bob --password-file ./bob.password
auth servers add storage-a --namespace studio
auth tokens create storage-a --server --name daemon --out ./storage-a.token
auth namespace-admins set alice studioAdministrator/service tokens default to 90 days; --days accepts 1–3650.
tokens create accepts only auth administrators or registered storage servers.
Regular users must log in with their password. Only init issues the
nonexpiring bootstrap token. Token creation writes the secret file durably
before transmitting its digest; it never prints the secret. Choose a fresh
output path for a new token. A retry can reuse its existing token file.
Initialize storage and its separate security state explicitly:
scribe-server init --data-dir ./storage-data --config-out ./storage.toml
scribe-server auth-init --data-dir ./storage-data --namespace studio
scribe-server serve --config ./storage.toml --auth=127.0.0.1:7443 \
--auth-namespace studio --auth-ca ./ca.pem \
--auth-token-file ./storage-a.token \
--tls-cert ./storage.pem --tls-key ./storage-key.pem--listen still uses the existing bind-address syntax, such as
tcp://0.0.0.0:7447; with auth enabled that socket serves TLS only. The
runtime listen file reports tls://host:port.
Alternatively add this section to storage.toml. File paths are relative to
the configuration file; CLI paths are relative to the working directory.
[auth]
endpoint = "127.0.0.1:7443"
namespace = "studio"
ca = "ca.pem"
token_file = "storage-a.token"
tls_cert = "storage.pem"
tls_key = "storage-key.pem"All six settings are required together. Missing or partial configuration
fails startup. auth-init durably marks the root with the required feature
auth-ownership-v1 before creating the ownership database, so older binaries
refuse to open it. A marked root or one containing auth-state.redb refuses unauthenticated
startup. Auth-state namespace changes require an explicit migration; changing
a config string cannot silently remap grants.
Create a repository and grant access to its ID:
scribe --server tls://localhost:7447 login alice \
--auth tls://localhost:7443 --auth-ca ./ca.pem
scribe --server tls://localhost:7447 repo create game
scribe --server tls://localhost:7447 repo list
auth grants set bob studio/REPOSITORY_ID --role reader
auth grants list --repo studio/REPOSITORY_IDUse the actual full ID printed by repo list. The first connection prompts
for trust as described below. Subsequent client commands reuse the saved
session automatically. --token-file / SCRIBE_TOKEN_FILE remain explicit
overrides for administrator credentials or separately managed session files.
The workspace stores its tls:// server URL but no credential. Supplying
credentials with a tcp:// URL is an error; there is no downgrade fallback.
Password pepper ownership
init --pepper-out FILE creates a random 32-byte pepper as 64 lowercase hex
characters in a new protected file. The file must be outside the auth data root
and any .scribe directory, including through symlinks. serve --pepper-file FILE
requires that same key on every start and refuses a missing, malformed, or wrong
key. The database stores only its full BLAKE3 fingerprint. Clients and storage
servers never receive the pepper; it is not embedded in PHC strings or logs.
Unix files use mode 0600; Windows inherits the parent ACL.
Keep a protected backup of the pepper separately from auth database backups. A database-only leak then does not allow password guesses without the pepper. Losing it prevents password verification: restore the matching key. Replacing it is not a rotation mechanism; there is no automatic rotation or migration. A compromised pepper requires a planned credential reset and reprovisioning. Earlier unreleased auth roots are rejected by the new format marker; create a fresh test auth root explicitly. Never delete an existing root to fix startup.
User login and session lifetime
The administrator provisions an initial password using users password and
delivers it securely. That command also resets a password and invalidates all
previous user sessions by changing the user's password generation; it never
changes the user's immutable ID or grants. Password hashes are excluded from
user listings, audit rows, and logs. The administrator sends the password only
through verified TLS; the auth server hashes it using Argon2's secret-input
parameter and its pepper, outside the database write transaction. The client
journal stores a BLAKE3 password commitment keyed by the administrator token;
the server's durable operation intent is keyed separately by the pepper. Neither
journal contains plaintext or an unkeyed password verifier. An unresolved retry
checks the original outcome first; if unknown, it requires the unchanged password
file. Administrator tokens remain independent of user sessions.
Even an auth administrator's password-issued session cannot invoke auth
administration; use the separate administrator token for that purpose.
Configure the lifetime of new sessions on the auth service:
scribe-auth serve --data-dir ./auth-data --pepper-file ./auth.pepper --listen 127.0.0.1:7443 \
--tls-cert ./auth.pem --tls-key ./auth-key.pem --session-ttl-days 180--session-ttl-days defaults to 90 and accepts 1–3650. The auth server chooses
and durably records an absolute expiry. Clients cannot request a longer TTL.
Restarting the service or changing this flag does not extend existing sessions.
There is no automatic renewal: the client asks the user to log in again after
expiry. Token revocation, a disabled user, or a password reset also prevents
access (subject to the existing authorization-cache bound of 30 seconds).
scribe login USER --auth tls://HOST:PORT prompts for Password: with input
hidden, then saves a session
for the selected --server (or workspace server), under the per-user config
root's sessions/ directory. Use --password-file FILE for automation or
redirected input; without that flag, a terminal is required. The prompt is on
stderr, so stdout remains command output (including with --json). Passwords
are never command-line arguments, echoed, logged, or saved in session state.
On macOS, interactive entry also acquires Secure Event Input for the duration
of the password read and releases it before authentication. A failure to enable
it aborts the prompt. Normal completion, input errors, and cancellation release
our assertion; panic unwinding also restores it. Other applications' Secure Input
assertions are preserved. Windows/Linux retain their native echo-disabled input;
there is no portable terminal API providing equivalent keyboard interception
protection. For SSH, keyboard protection belongs on the local computer.
The password file is not copied into session state. New directories/files use 0700/0600 on Unix;
Windows inherits the parent ACL. No credential is saved under .scribe/.
Pass --auth-ca FILE for explicit auth-service trust, or approve its CA using
the same first-use trust mechanism as storage connections. Passwords are sent
only after TLS verification. Auth and storage endpoints have separate trust.
Log out with:
scribe --server tls://localhost:7447 logout --auth-ca ./ca.pemLogout revokes the current session at auth before removing the saved credential. A connection failure keeps the pending operation; rerun the same command. Commands refuse to use a session with a pending logout. Logging in again creates a new session; sessions on other machines remain valid until revoked, reset, disabled, or expired.
For durable retries without storing recoverable bearer tokens at auth, login
creates a random token candidate and operation ID locally, flushes both, and
sends the candidate's digest with the username and password. Successful password
verification accepts that digest with a server-selected expiry. The token is
usable only after this durable acceptance. A retry first requests
login-outcome; it returns the original expiry and does not mint another token.
The password is not part of persisted intent/outcome data. Saved sessions are
scoped to the normalized storage hostname and port, and retain the auth endpoint
needed for logout. Explicit --token-file / SCRIBE_TOKEN_FILE takes precedence.
Login hashing runs on an admitted blocking worker, outside the publication gate and database write transaction. Only one hash verification runs at a time; concurrent logins receive a retryable busy error instead of occupying every worker. At most ten attempts per exact username are admitted per 60 seconds; the bounded, 4096-entry attempt table is in memory and resets on service restart. Unknown users undergo a dummy Argon2id verification and receive the same error as an incorrect password. Password state and enabled status are rechecked after hashing under the publication gate so a concurrent reset/disable cannot race session issuance.
Auth protocol and storage additions (unreleased schema 1 / SAU1)
The SAU1 request layout remains a str_u16 credential followed by a 16-byte
operation ID and bounded string arguments, all lengths little-endian. The
credential bound is now 1024 UTF-8 bytes. login / login-outcome use the
credential field for the password; every other command requires a 64-character
lowercase hexadecimal bearer token. Login arguments are [command, username, token_digest, label], with a nonzero persisted operation ID. A successful login
or known outcome returns [["ok", expiry_unix_ms]]; an unknown outcome returns
no rows. logout / session-outcome take only the command argument and return
[["ok"]] or no rows for an unknown outcome. users-password arguments are
[command, username, password_commitment, random_generation_id], followed by
one transient str_u16 new password (12–1024 UTF-8 bytes). That extra field is
exclusive to users-password and is excluded from persisted argument encoding.
The commitment is keyed BLAKE3 over the password with a key derived from the
administrator token using context Scribe auth admin password commitment v1.
The durable intent uses canonical encode_intent with a zero operation ID and
all four arguments, followed by password bytes, keyed using the pepper and
context Scribe auth password operation intent v1. Both keys use BLAKE3's
derive_key. Password changes use the existing durable administrative-outcome
path; replay cannot replace the original password.
The SCRIBE_AUTH marker is scribe-auth\nschema=1\nprotocol=1\npassword=argon2id-pepper-v1\n.
Additional auth_v1 keys use the existing single-row SAU1 response encoding:
| Key | Row |
|---|---|
meta/pepper-id | full BLAKE3 fingerprint of the raw 32-byte pepper |
passwords/USERNAME | peppered Argon2id PHC string, random 128-bit generation as lowercase hex |
tokens/DIGEST for sessions | session, username, expiry Unix ms, enabled, label, password generation |
sessions/OPERATION_ID | immutable user ID, BLAKE3 intent digest, ok, original expiry Unix ms |
session-logout/OPERATION_ID | token digest |
The login intent hashes canonical encode_intent bytes with a zero operation
ID and [username, token_digest, label]; no password bytes are hashed into it.
Administrator/service token rows retain five fields. Session rows carry six.
Each session mutation publishes its token row and outcome in the same immediate
transaction before acknowledging. Repository/object formats and storage-protocol
frames are unchanged.
Remembered client trust
On first connection, scribe completes a TLS-only discovery handshake and
shows the server CA fingerprint. Type yes to approve and save it. There
is no CA download/import step. Discovery sends no Scribe frames or tokens;
after approval is durably saved, the client reconnects and verifies TLS using
that saved CA before sending credentials. Hostname, certificate validity,
chain, and handshake signatures are checked, including during discovery.
This is trust on first use: an attacker controlling the first connection can
present their own CA. Compare the displayed fingerprint with the value your
administrator provides through a trusted channel to authenticate that first
connection. Fingerprints are full BLAKE3 digests of the CA's DER certificate
bytes, prefixed with BLAKE3:. Approval is scoped to the normalized hostname
and port; another server using the same CA does not inherit that approval.
Trust is shared across the user's workspaces and stored outside .scribe/:
- macOS:
~/Library/Application Support/Scribe/trust - Windows:
%LOCALAPPDATA%\Scribe\trust - Linux:
$XDG_CONFIG_HOME/scribe/trust, or~/.config/scribe/trust
--config-dir or SCRIBE_CONFIG_DIR overrides the shared Scribe user
configuration root; trust lives in its trust/ subdirectory, leaving the
same root available for future user settings. New directories/files are
private on Unix; Windows uses the parent directory's ACL. Records contain
the endpoint and public CA certificate, never a token. Corrupt records fail
closed and are never silently replaced.
Trust can also be managed without a token or application connection:
scribe --server tls://localhost:7447 trust add
scribe --server tls://localhost:7447 trust show
scribe --server tls://localhost:7447 --trust-fingerprint BLAKE3:OLD_DIGEST trust forgettrust show is offline. trust forget requires the exact saved fingerprint
to prevent accidental removal. If the CA changes, connections fail without
sending a token or modifying trust. Verify the change with the administrator,
forget the old trust explicitly, then approve the new CA. Routine leaf
certificate renewal under the saved CA needs no new approval.
Noninteractive commands and --json never prompt or automatically accept.
Preapprove using trust add --trust-fingerprint BLAKE3:EXPECTED_DIGEST, or
supply that flag on the first normal command. A mismatching fingerprint
fails and cannot replace an existing trust record. The fingerprint is public
and suitable for a CI configuration variable.
Explicit --tls-ca / SCRIBE_TLS_CA remains available for managed deployments;
it uses the supplied CA instead of remembered trust. Storage-to-auth and
auth-administration connections still use their operator-configured
--auth-ca; users of the normal scribe client do not need that file.
The storage executable also offers these management conveniences using operator credentials, independently of the daemon's service credential:
scribe-server users --auth 127.0.0.1:7443 --auth-ca ./ca.pem \
--admin-token-file ./admin.token list
scribe-server permissions --auth 127.0.0.1:7443 --auth-ca ./ca.pem \
--admin-token-file ./admin.token set bob studio/REPOSITORY_ID --role writerAdd another registered server, service token, and initialized storage root pointing to the same auth endpoint to share users across instances.
Administration and retries
users enable/disable, servers enable/disable, tokens revoke DIGEST,
grants revoke USER REPOSITORY, and namespace-admins set/revoke provide the
corresponding lifecycle operations. users add --administrator creates
another central administrator. tokens list shows digests, not tokens.
Lists return at most 256 rows. Their first column is the pagination cursor;
pass the last row's key as --after KEY to continue. audit --after KEY
lists administrative results with timestamp, immutable actor ID, command,
operation ID, and status. Exact request fingerprints and outcomes are kept
in the operation table; audit rows contain no credentials.
Administrative mutations persist their operation ID and exact request under
$HOME/.scribe/auth-admin (or --state-dir) before transmission. Unknown
outcomes retain that pending file. Repeating the same command resends the
same operation after querying operation_outcome; auth returns its durably
recorded outcome. operations outcome OPERATION_ID also queries it explicitly. Reusing an ID
with another actor or intent is refused. Confirmed outcomes remove the local
pending request. Local administrative invocations use an OS lock to avoid
racing token files or pending requests.
Sessions, revocation, and failures
Storage caches each positive or negative authorization decision per credential/session and repository for at most 30 seconds, capped by both user-token and service-token expiry. Cache expiry starts before sending the auth request. No remote auth I/O occurs inside repository publication gates or redb write transactions. Cache size is bounded to 256 entries per connection, and the existing storage connection cap still applies.
New sessions validate online. During an auth outage, existing requests can
use unexpired leases. Expired leases return auth_unavailable; no cached
permission is extended on failure. Disabling a user/server, revoking a token,
or changing a grant affects new operation admissions within 30 seconds.
Already admitted requests may finish, including a durable submit or streamed
transfer. This does not promise instantaneous cancellation of in-flight work.
Auth has one process and one database owner; multiple replicas/automatic
failover are not implemented. Back up the complete auth directory while the
service is stopped, retaining the protected credentials separately.
Each secure control session returns a random 256-bit binding secret. Up to four additional data connections must authenticate with the same credential, prove the binding secret, and reference a live control session on that server. Closing the control connection stops further requests on its data connections.
Protocol v3 uploads have a separate admission response before raw bytes: a reader cannot send an asset upload and force the server to consume it. All object payloads pass through TLS; the plaintext sendfile path is used only by unauthenticated TCP2 connections.
Ownership and existing data
Storage keeps a separately versioned auth-state.redb (schema 1) in its data
root, protected by the same root OS lock. Workspace ownership and operation
owner/intent claims commit with immediate durability before the corresponding
repository mutation. A crash may leave an unused claim, but it never accepts
history. Repeated claims must match the same immutable user and intent.
Accepted-history durability and existing operation outcomes are unchanged.
The authenticated user supplies commit and lock attribution. A client
--author cannot impersonate another user. Workspace IDs and operation IDs
are never proof of ownership. Existing workspaces from unauthenticated use
have no trusted owner and cannot be adopted by first use; create a fresh
workspace and preserve/reapply local edits explicitly. Existing history
remains readable under repository grants.
Storage backup/restore includes auth-state.redb. Do not remove it to bypass
authentication or copy only repository metadata when recovering secured
working-copy/operation ownership.
Test coverage for all of the above is listed in testing.md.
For TOML-based local cluster startup, pepper provisioning, and idempotent user bootstrap, see the cluster guide.