Ověřování a přístup
Dokumentace/Provoz serveru
Ověřování a přístup48 min čtení

Shared authentication and repository permissions

Zdrojová dokumentace je zatím dostupná v angličtině. Ovládání čtečky odpovídá zvolenému jazyku.

Na této stránce

For every service and administration command and option, see the auth CLI reference. Client login and trust options are in the client CLI reference. This guide explains identities, permissions, TLS trust, and credential lifecycles. scribe-ctl sets all of this up for you (tutorial 2: add authentication); to do it by hand, follow bare metal, stage 2 or Docker Compose, stage 2.

Every command-line tool except scribe-runner supports global --json; see JSON output for automation for results, errors, and exit codes. For scribe-runner, --log-format json formats its logs only.

Scribe can use one scribe-auth service for several independent storage servers. Auth owns users, credentials, server identities, grants, and the two certificate authorities that certify credential holders. Storage servers own repository data and enforce permissions on each operation. Auth does not receive repository contents or check whether a repository exists.

Authenticated storage connections use protocol 4 over TLS 1.3; the auth service uses its own binary protocol, SAU2, over TLS 1.3. Every Scribe TLS endpoint (auth, storage, gateway, and the trust-discovery handshake) offers and accepts only the hybrid post-quantum key exchange X25519MLKEM768; a peer without it cannot connect. An authenticated listener never negotiates plaintext or an older protocol. Callers prove who they are with a client certificate in the TLS handshake; no password-derived or bearer secret travels to a storage server or gateway. For implementation details, see architecture and the wire specification.

Every certificate authority and server key Scribe creates is ML-DSA-65 (FIPS 204), a post-quantum signature. That covers the server CAs, auth's credential and access CAs, and every server's TLS key. Recovering one of those private keys from its public certificate would let an attacker impersonate a server or issue certificates, and ML-DSA resists that with a quantum computer as well. Device keys stay ECDSA P-256 (see below). Their public keys are hard to reach: TLS 1.3 encrypts client certificates under the hybrid key exchange, and auth stores only a digest of each key. A forged device signature would also need a quantum computer during the live handshake. A CA created before ML-DSA, or one an operator made with another algorithm, keeps working; rotating it gives an ML-DSA-65 successor.

Credentials and roles

Every account has an immutable random 128-bit ID, an immutable username, enabled status, a separate auth-administrator flag, and a kind: person or service. A person is a user; a service account is a machine identity that authenticates only with credential files. Regular users log in with a username and password to obtain a 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.

A credential is a device key its holder generated, registered at auth and certified by it (see identity, device keys and certificates). Credentials are individually revocable and expire. Disabling a user revokes every credential it holds, so enabling the account again revives none of them. The initial admin identity is reserved and cannot be disabled; its bootstrap credential can be revoked after another administrator credential 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. A user principal name (UPN) is accepted as a Scribe identifier; Scribe does not connect to Active Directory or verify a mailbox. Certificates never carry a username; they name the immutable account ID.

The administration examples use the auth shell function defined in authentication tutorial:

sh
auth users add alice.smith@company.example
auth users password alice.smith@company.example --password-file ./secrets/alice.password
auth grants set alice.smith@company.example studio/REPOSITORY_ID --role writer

Each registered storage-server identity belongs to exactly one namespace and has an immutable random ID that auth assigns when it is added. Give each server its own revocable server credential, even when several servers share a namespace. A server credential can validate callers and read decisions only for that namespace. It cannot list users, issue credentials, or edit grants. (Server credentials identify storage servers; service-account credentials identify a service account and are used like a user's credential.) User credentials are not bound to a namespace: one login is valid wherever the user holds grants, which is what lets a single gateway front several namespaces and address their repositories as <namespace>/<name>.

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.

RoleAllowed
ReaderRepository discovery and metadata, history, object presence/download, workspace registration, branch and merge reads
WriterReader plus object uploads, submits, branch create/rename, own workspace locks
MaintainerWriter plus other-owner workspace listing/retirement and generation-checked forced unlock and changes to the repository kind, the ordered locks.rules, and the mount topology in .scribe/config.toml on main (including merges and removal)
Namespace readerReader on every repository in that namespace, including repositories created later, and the namespace-wide reads: the storage inventory a gateway or controller polls
Namespace administratorAll repository roles in that namespace, the namespace-wide reads, and repository creation
DelegateA service account (a gateway) that may present users' access certificates to the storage servers of that namespace; see gateways and delegation
Auth administratorCentral user/credential/server/grant administration; no implicit storage access
Service accountAt most Writer, through its grants and its credential's ceiling; never Maintainer, namespace or auth administration, or sign-in

A grant gives one account one role on one repository key. There are no groups and no path-level permissions: a role applies to the whole repository. Grant each account separately on each repository, or grant a namespace-wide role.

A namespace reader grant (namespace-readers set USER NAMESPACE) gives Reader on every repository key in the namespace, including repositories created after the grant, without a grant per repository. Any account kind can hold it. The effective role is the stronger of the direct grant and the namespace reader grant, so a direct writer grant still writes; a namespace reader grant alone never writes, creates repositories, or administers.

Granting namespace administration is an explicit, separately audited command. Repository maintainers do not gain permission to manage central users/grants. Writers may change profiles on main and submit lock-rule proposals on other branches. On main, only changes to the decoded ordered lock-rule strings require a Maintainer; formatting, comments, and profile-only edits do not. Deleting nonempty rules, their section, or the config file counts as a lock-policy change. A merge whose .scribe/config.toml entry keeps main's configuration changes nothing on main and needs no Maintainer. Only the accepted main-branch copy controls repository-wide locking. Policy queries require Reader; lock acquisition requires Writer and workspace ownership. Main-policy write permission is checked through AuthProvider before entering publication gates. Private repository access without a matching grant is denied. Public visibility adds anonymous read access; writes still require an authenticated grant. See public repositories for policy administration, downloads, and revocation semantics. Repository discovery filters out inaccessible repositories. Operation outcomes are visible only to their recorded user, who must still have repository read access; another user's operation ID is answered exactly like an unknown one. Permission to edit a change note follows the owner recorded by the submit operation that accepted the change, not the author text shown in history.

Identity, device keys and certificates

Every credential holder generates its own device key: a scribe login on a workstation, a web session, a CI machine, a runner, a storage server, a gateway, and an administrator each hold a different one. Keys are ECDSA P-256 (recorded as algorithm p256 with every credential). The private key never leaves its holder. Auth stores only the key ID, the full BLAKE3 digest of the public key's SubjectPublicKeyInfo, and registers each public key at most once.

Buffers that hold a private key are wiped when they are dropped: device keys, the PEM and credential-file text written or read for them, the CA keys auth and scribe-cert load, and a server's TLS key file. Scribe decodes its own PKCS#8 key files itself, so no parser keeps an unwiped copy. TLS signs with the key object AWS-LC builds from them, and AWS-LC wipes that object when it frees it. Two cases keep an unwiped copy. A server key in another encoding (SEC1, PKCS#1, or a key next to other PEM sections) goes through the general PEM reader. Auth's CA keys also stay in its database file and that file's page cache.

Auth runs two private client certificate authorities with ML-DSA-65 keys, created by init (or migrate) and stored in the auth database. It issues two kinds of X.509 certificate for the same registered key:

CertificateIssued byLifetimeAccepted by
Credential certificatecredential CAthe credential's: a session's 90 days by default, a machine credential's until its expiry; the bootstrap administrator's never expiresauth only
Access certificateaccess CAone hour, never beyond the credential's expirystorage servers and gateways only

A certificate from one CA is refused by the other kind of service, so nothing a storage server or gateway receives can act at auth. Clients never create certificates; auth issues all of them for a public key the client supplies. A holder presents its credential certificate to auth and asks it for access certificates (access-certificate, which writes nothing at auth), then presents the access certificate to storage servers and gateways. Storage servers themselves use only their credential certificate, towards auth.

Each certificate names exactly one SPIFFE identity in a URI subject alternative name and has an empty subject:

text
spiffe://<trust-domain>/user/<user-id>/credential/<credential-id>
spiffe://<trust-domain>/server/<server-id>/credential/<credential-id>

People and service accounts share the user ID space; storage servers use server. IDs are 32 lowercase hexadecimal characters; a username never appears. The trust domain is chosen once with scribe-auth init --trust-domain (default scribe; 1–64 lowercase letters, digits, ., -, _). Certificates certify the holder's P-256 key under the issuing CA's ML-DSA-65 signature, and carry a random serial, CA:false, the digitalSignature key usage and the clientAuth extended key usage. A certificate a Scribe peer sends or stores is at most 16,384 bytes of DER.

The certificate is not the whole check. Every verifier also asks auth whether the certificate's credential ID is live and whether the certificate's public key is the one registered for it, and whether the account or server is enabled. Revocation therefore takes effect within the 30-second authorization lease whatever the certificate's lifetime, and a leaked CA key alone certifies nothing usable. TLS 1.3 signs the handshake with the device key, so the proof is bound to that connection and cannot be replayed elsewhere.

Clocks are tolerated rather than trusted. Auth backdates every certificate by 10 minutes and verifiers accept 5 minutes of skew past its validity. Clients renew an access certificate once half its lifetime has passed as measured by their own clock between receipt and now (a clock that jumps backwards also renews), so a drifting wall clock never causes hourly failures. Long-running services (storage servers and gateways) refresh auth's access CAs every five minutes and refuse presented certificates until the first refresh succeeds.

Rotating the internal CAs

Both CAs are valid for 20 years and may be replaced without users noticing: auth trusts every CA row it holds (trust-bundle lists them all), and a CA added later starts signing 15 minutes after it was added, by when every running verifier has refreshed its trust. The oldest CA of a purpose is retired only once nothing it signed can still be presented (or, for the credential CA with --revoke-held, together with revoking what still holds one of its certificates; its holders sign in again), and a retirement moves the revision like any other security-reducing change. scribe-auth authorities list|add|retire does this offline, and scribe-ctl renew --rotate-ca credential|access in a managed cluster; see auth's internal CAs.

Credential files

Administrators, machines, storage servers, gateways and scribe-ctl keep a credential in one text file, the credential bundle:

text
scribe-credential v1
auth tls://auth.example:7443
-----BEGIN PRIVATE KEY-----        the device key (PKCS#8)
-----BEGIN CERTIFICATE-----        the credential certificate
-----BEGIN CERTIFICATE-----        the CA of the auth service's TLS certificate

The file says where its holder reaches auth, holds the device key and the credential certificate auth issued for it, and the CA that verifies the auth service. Parsing is strict: the two header lines, then exactly those three PEM sections in that order. scribe-auth init, migrate, restore, admin-credential issue and credentials create write these files: always new files, never overwritten, mode 0600 on Unix, and never inside any .scribe directory. On Unix, every reader refuses a credential file that grants any permission to group or other (… is accessible to other accounts (mode 644); run chmod 600 on it) or whose directory other accounts can write, unless that directory is sticky (… is writable by other accounts …; restrict it with chmod go-w). The same rule applies to the pepper, the revision witness, session device keys and the runner's token and device key. On Windows the same rule reads the access list: a file that allows any account other than its owner, the reading account, SYSTEM and Administrators more than reading its attributes is refused (… is accessible to another account (S-1-…)), as is one whose directory lets such an account delete its entries or change its access list. Scribe writes these files with a protected list allowing only the writing account, SYSTEM and Administrators.

HolderHow it reads its credential file
Auth administratorscribe-auth --admin-credential FILE (also scribe-server users/permissions)
Machine or CI jobscribe --credential-file FILE or SCRIBE_CREDENTIAL_FILE
Storage server[auth] credential / --auth-credential
Gateway[tls] credential
scribe-ctl[auth] admin_credential, [[storage]] credential and monitor_credential

A credential file is as powerful as the account it names until it expires or is revoked. Anyone who can read it can act as its holder; revoke it with credentials revoke when it may have been copied.

Service accounts

A service account is an account of kind service for build farms, CI, and other machines. It has the same username rules and ID space as users, so its name appears in grants, audit rows, and submit attribution. It has no password and never signs in; it authenticates only with credential files that an auth administrator issues:

sh
auth users add build-farm --service
auth namespace-readers set build-farm studio
auth credentials create build-farm --service --name ci-read --out ./secrets/build-farm.credential --days 90

Point the machine at the file with scribe --credential-file FILE or SCRIBE_CREDENTIAL_FILE. A plain --service credential is a service-reader credential and never writes. --service --write issues a service-writer credential, which writes only where the account also holds a writer grant:

sh
auth grants set build-farm studio/REPOSITORY_ID --role writer
auth credentials create build-farm --service --write --name ci-write --out ./secrets/build-farm-write.credential --days 30

Service accounts are deliberately limited:

  • no password: users password on one is refused, and login is refused with the same answer as a wrong password;
  • never an auth administrator (users add --service --administrator is refused) and never a namespace administrator;
  • grants are at most writer; a maintainer grant is refused;
  • credentials expire within 365 days and are revocable one by one; disabling the account revokes all of them. credentials list --user NAME lists them;
  • a service-account credential cannot call identity, create automation credentials, log out, or perform any auth administration.

Namespace reader grants are the usual way to give a service account read access to a whole namespace, including repositories created later.

Every machine identity is a service account with the narrowest grant its job needs, never an administrator: the web services read with a namespace reader grant, scribe-ctl takes inventory with one, and a gateway holds a namespace reader grant and a delegate grant (see namespace-wide operations and gateways and delegation).

Gateways and delegation

A gateway routes unchanged clients to storage servers. Clients connect to it exactly as they connect to a storage server, with their own access certificate; the gateway verifies that certificate against auth's access CAs. Towards each storage server the gateway connects with its own access certificate and says, in the protocol 4 hello, how to read the caller:

  • delegated, for user traffic: the hello carries the user's access certificate, which the storage server verifies against the access CAs; auth then checks that the user's credential is live and that the gateway's account is a delegate in that server's namespace;
  • direct, for the gateway's own inventory polling, under its own identity;
  • anonymous, for anonymous readers of public repositories.

The gateway's identity is a service account. For every namespace it fronts it needs a namespace reader grant (namespace-readers set, to poll inventory) and a delegate grant (delegates set, to act for users), and it holds a service-account credential; bare metal, stage 3 provisions it. delegates set accepts only service accounts. Storage servers decide everything about delegated traffic as if the user had connected directly: the user's grants, the user's attribution, and limits charged to the user rather than to the gateway. A delegated decision's lease is also bounded by the gateway credential's expiry. A compromised gateway can act only for users whose access certificate it currently holds, at most for that certificate's remaining hour, and only in the namespaces where it is a delegate. It never receives anything that works at auth or at another gateway. scribe-ctl setup provisions this account for a managed gateway; see the cluster guide.

Namespace-wide operations

A few storage operations concern a namespace as a whole rather than one repository. The storage server gates each through one rule, and every such operation names which it needs:

OperationNeeds
Instance inventory (instance_status): every repository's ID, name and size, and the data root's used and free bytesReading the namespace: a namespace reader grant or namespace administration
Repository creation (repo_create)Administering the namespace: namespace administration

A service account reads a namespace only through its own namespace reader grant; it is never a namespace administrator.

Setup

scribe-ctl provisions auth for a cluster it manages: scribe-ctl add auth puts it in front of existing storage (tutorial 2), and scribe-ctl setup --variant auth builds a new cluster with it (set up a new cluster). The sections below explain the rules behind each step; for every option, use the auth CLI reference.

Set up auth by hand

The steps are in bare metal, stage 2 and, for containers, Docker Compose, stage 2. In outline: two server-certificate CAs and a certificate for each service; scribe-auth init, which writes the pepper, the revision witness and the bootstrap administrator credential; scribe-auth serve; for each storage server an identity (servers add) and its own credential (credentials create --server); scribe-server auth-init and the storage [auth] table; then people and grants.

Generate a private CA and server certificates

Certificate generation is built into Scribe; no external certificate tool is required. These are the services' own TLS server certificates, separate from the two client CAs auth keeps in its database. Because that database holds the client CAs' private keys, init creates the auth data directory with mode 0700, and every auth command refuses (restrict it with chmod 700 DIR) a data directory other Unix accounts can enter or list; scribe-ctl plans and up check the same before they start auth. The auth service, storage server, and cluster controller use the same certificate validation rules. Existing CA certificates are checked for signing permissions, validity, key pairing, and self-signature before issuing a server certificate. Generated server certificates are verified through rustls for every requested DNS/IP name.

Both service CLIs provide the same helper; all flags are in the certificate reference. Use a separate CA for the auth service's certificate, as scribe-ctl does ([certificates] auth_ca_dir): a storage or gateway key issued by the other CA can then never pass as auth. The certificates guide shows both server CAs and auth's two internal CAs side by side.

Use 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. The CA can sign leaf certificates and exactly one intermediate below it (path length 1): the handover certificate of a CA rotation. Each issue directory contains server.pem, server-key.pem, and a copy of ca.pem. The CA and server keys are ML-DSA-65; issue also accepts a CA key in another algorithm (ECDSA, Ed25519, RSA), and the server key it creates is ML-DSA-65 regardless. Issued certificates permit server authentication only. The helper verifies the certificate chain and key pair before reporting success. It does not create users or credentials.

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 the directory gets a protected access list allowing only the creating account, SYSTEM and Administrators, and the files inherit it. Private keys are unencrypted so services can start unattended. Keep the CA signing key (ca-key.pem) offline and backed up; give each server only its own leaf key/certificate and the public CA certificate. 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. Renewing a certificate from the same CA needs no new trust on any client; replacing the CA does, unless it is rotated with a handover (certificates and credentials). 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.

Bootstrap credentials and the storage binding

init writes the bootstrap administrator credential, the pepper and the revision witness to the files it is given. The administrator credential is an operator credential, not a default password or a storage server credential. Only init issues a nonexpiring bootstrap credential. Every later administrator, server, and service-account credential expires. credentials create generates the device key on the administrator's machine and writes it durably beside the pending request before transmitting only the public key; the credential file is completed after auth answers. An interrupted run is resolved by repeating the same command, which certifies the same key.

auth-init runs while storage is offline. It marks the root with the required feature auth-ownership-v1 and creates ownership state. A marked root or one containing auth-state.redb refuses plaintext startup. Namespace changes require an explicit migration; changing a configuration string cannot silently remap grants. Storage requires all four [auth] settings together (namespace, credential file, TLS certificate and key); the server configuration describes their TOML form. The storage server learns the auth endpoint and auth's CA from its credential file, verifies clients against the access CAs it fetches from auth, and asks auth about each caller with its own credential certificate.

For auth admission limits and required flags, see serve and service admission limits. TLS handshakes and incomplete frames do not occupy storage workers. Response writes and worker admission have a 10-second timeout. Shutdown closes idle connections; after 30 seconds it interrupts remaining network I/O and waits for started storage jobs.

Service admission limits

Every storage server authorizes each request through the auth service, so a peer that can merely reach the auth port must not be able to stall it. The service therefore bounds what an unauthenticated connection and a login can hold:

LimitValueWhy
Handshake and first request3 seconds, one deadlineUntil a complete request frame arrives the peer is anonymous; a silent or trickling connection releases its slot quickly.
Idle connectionclosed after 60 secondsA connection carries sequential requests, so storage servers keep a small pool instead of a handshake per decision.
Incomplete connections per source16 (--max-pending-per-source)One address cannot fill the connection slots. IPv6 sources are counted per /64. An idle connection between requests counts as incomplete again.
Full serveroldest incomplete connection closedAnonymous or idle connections never keep a newcomer, such as a storage server, out; they can delay it by at most one handshake. A newcomer is refused only when every slot carries a request in progress.
Concurrent password hashes2, with 32 waitingLogins wait for a hasher without occupying a storage worker or a thread, leaving workers for authorization. A full queue answers auth_unavailable ("login busy; retry later").
Login attempts per source and username10 per minuteA guessing source is throttled for that account without locking the account out for anyone else.
Login attempts per source60 per minuteOne source cannot spray passwords across many accounts.
Login attempts per username, all sources100 per minuteA ceiling against distributed guessing that no single source can reach on its own. A source that signed in successfully as that user within the last 30 days is exempt, so guessing elsewhere cannot lock the owner out.

Refused attempts answer auth_unavailable ("too many login attempts; retry later") before any password is hashed, and each window resets a minute after its first attempt. Attempt tables are bounded; when one is full, the oldest window is forgotten rather than a new source or username refused. Connection limits count per address as the auth service sees it, so a NAT or a proxy counts as one source: the web interface signs users in from its own address and should rate-limit its callers itself. Raise --max-pending-per-source when several storage servers share an address. Refused and evicted connections are logged at debug level as connection.refused and connection.evicted. A failed sign-in logs login.failed, a refusal by the per-username ceiling logs login.throttled (warning), and an issued session logs login.succeeded; each names the username, never the password.

Diagnostic selection, daily files, retention, and shutdown flushing are described once in the logging guide. Auth lifecycle events include auth.listening, auth.stopped, and auth.failed. These logs are independent of durable administrative audit records.

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 (the whole namespace, at any level), 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; on Windows they allow only the writing account, SYSTEM and Administrators.

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. An unsupported auth format is refused. Preserve the data root and its matching pepper when investigating a startup failure.

Upgrading the auth data directory

Normal startup never converts auth data. A directory written by an older build is refused with auth schema 3 is not supported by this build (supports 4); run `scribe-auth migrate` explicitly. To upgrade:

sh
# stop scribe-auth serve, then back up the whole auth directory
scribe-auth migrate --data-dir ./auth-data --dry-run
scribe-auth migrate --data-dir ./auth-data --revision-file ./secrets/auth.revision \
  --admin-credential-out ./secrets/admin.credential \
  --auth tls://auth.example:7443 --auth-ca ./pki/ca/ca.pem

migrate takes the directory's exclusive lock, so it fails while the service runs. --dry-run validates every record it reads and reports what would change, writing nothing. A real run always names the revision witness (--revision-file), which a migration rewrites; --admin-credential-out, --auth and --auth-ca are needed only from schema 3, and a run from schema 3 without them is refused. scribe-ctl upgrade runs auth migrations itself (dry run, backup, migration, rollback with the witness), except this one: it refuses schema 3 to 4, which replaces every credential and is done by hand as below. Only schema 3 upgrades to schema 4. The upgrade keeps every account, password, grant, namespace role, visibility policy, storage server (each is assigned an immutable random ID) and audit row. Bearer tokens cannot become key credentials, so every token, session and automation token is removed: people sign in again and every machine, storage server and gateway is issued a new credential file. The upgrade creates the trust domain (--trust-domain, default scribe), both client CAs and the rollback state, and writes a new credential for the bootstrap admin account (valid 3650 days) to --admin-credential-out, using --auth and --auth-ca as the endpoint written into it. A malformed user or server record aborts the run before any write.

The credential file is written and flushed first, then every converted record commits in one durable transaction, then the revision witness is written, and the schema marker is replaced last by an atomic rename. A rerun after the records committed only finishes the witness and marker. A rerun that finds the credential file but no converted records refuses and names the file: remove it and rerun. A directory that is already current reports that there is nothing to do. The pepper file is not read. Start the upgraded service with serve --revision-file naming the new witness.

User login and session lifetime

How people log in, log out and check their session is in log in; this section describes what happens underneath. scribe whoami is local status only; it does not verify server-side revocation. Its --json form includes anonymous and credential-file overrides.

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 and automation credentials by changing the user's password generation; auth refuses a change that would keep the old 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 administrator's local journal stores a BLAKE3 password commitment keyed by a key derived from the administrator's device key; it never leaves that machine. 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 credentials remain independent of user sessions. A password-issued session never invokes auth administration, even an auth administrator's. Administration takes an administrator credential: a long-lived one from credentials create (for automation, scripts and scribe-ctl), or a short-lived one from administrator sign-in.

Configure the lifetime of new sessions with scribe-auth serve --session-ttl-days.

--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. Credential revocation, a disabled user, or a password reset also prevents access (subject to the authorization-cache bound of 30 seconds).

scribe login saves the session for the selected --server (or workspace server) under the per-user config root's sessions/ directory. Without --password-file, 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 Scribe's Secure Input assertion; panic unwinding also restores it. Other applications' Secure Input assertions are preserved. Windows and Linux use native echo-disabled input; there is no portable terminal API providing equivalent keyboard interception protection. For SSH, keyboard protection belongs on the local computer.

Login generates a new device key and a random credential ID on the local machine, flushes both with the pending operation, and only then sends the username, the public key and the password. Successful password verification registers the key as a session credential with a server-selected expiry and answers its credential certificate. A retry of a login whose answer was lost asks login-outcome first; it returns the original certificate and expiry and never issues a second session. The password is not part of persisted intent/outcome data. Logging in again for the same server revokes the previous session first; if that revocation cannot be completed, the new login still proceeds, a warning (session.revoke_failed) is logged, and the old session stays valid until it expires or is revoked.

A session directory holds the device key (key.pem), the credential certificate (credential.pem), the session's identity and expiry, the cached access certificate, and, when login --auth-ca was given, a copy of that CA (auth-ca.pem), never a path to it. Commands renew the access certificate from auth with the credential certificate when it is missing or past half its lifetime; parallel commands wait while one renews. Saved sessions are scoped to the normalized storage hostname and port, and retain the auth endpoint needed for renewal and logout. An explicit --credential-file / SCRIBE_CREDENTIAL_FILE takes precedence over the saved session.

The password file is not copied into session state. New directories/files use 0700/0600 on Unix and a protected access list for this account, SYSTEM and Administrators on Windows. A --config-dir / SCRIBE_CONFIG_DIR that already exists keeps its own permissions, so it must be user-private (the default under %LOCALAPPDATA% is). No credential is saved anywhere under .scribe/, at any level. 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.

scribe logout revokes the current session at auth before removing the saved session. It presents the session's own credential certificate and uses the CA copy saved at login; pass --auth-ca FILE to logout only to use a different file. A session whose credential already expired has nothing left to revoke and is removed locally without contacting auth. A connection failure keeps the pending operation; rerun the same command. Auth answers a logout of an already revoked credential as successful, so a retry always completes. A trust or configuration failure before the request leaves no pending operation. Commands refuse to use a session with a pending logout. Sessions on other machines remain valid until revoked, reset, disabled, or expired. Without a saved session, logout reports not logged in at SERVER and changes nothing.

A login whose answer never arrived is kept as an unfinished login for that username and auth endpoint until a retry resolves it or scribe login --discard drops it; every refusal caused by an unfinished login names that command. Discarding never revokes anything: a session the server may have issued for the lost answer stays unused until it expires.

Login hashing runs on an admitted blocking worker, outside the publication gate and database write transaction. 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. Each successful login is recorded in the audit log.

Administrator sign-in

Administrator sign-in (admin-login) is login's exchange with a different result: an auth administrator signs in with username and password, from scribe login --admin --out FILE or the web administration page, and receives a short-lived administrator credential instead of a session, the way sudo grants administration for a while after a password. No administrator has to fetch a credential file from the auth host first.

The password is checked exactly as for login: the same Argon2id and pepper, the same hashing admission and per-source and per-username attempt ceilings, and the same invalid username or password for an unknown, disabled or service account and for a wrong password. Only after the password is proven does an enabled person who is not an auth administrator hear auth administrator required (FORBIDDEN), and nothing is issued. For an administrator, auth registers the client-generated public key as an administrator credential of kind user, the kind credentials create issues for an administrator, labeled by the client (admin-sign-in from the CLI), with an expiry auth chooses: scribe-auth serve --admin-session-hours, 1–24 hours, 8 by default; clients cannot ask for longer. The answer is its credential certificate, and the audit log records it as admin-login with the username.

The credential is an ordinary administrator credential in every other way: it runs any administrative request, credentials list shows it, credentials revoke revokes it (it may revoke itself), and it counts toward the account's 256 live credentials. It also carries the password generation it proved, like a session, so a password reset invalidates it, and disabling the account revokes it with the rest. Because it lapses within hours and the password can sign in again, it is never kept as an administrator's last credential and never counts as the one that remains when another is revoked or a credential CA is retired.

The client persists the new device key and a random operation ID before sending (scribe login --admin under admin-sign-in/ in the per-user configuration root). Administrator sign-in shares login's operation space: a retry with the same ID returns the original certificate and never a second credential, an ID reused for another key or for a session login is refused as OPERATION_INTENT_MISMATCH, and admin-login-outcome resolves a lost answer without issuing anything. The web API holds its key in memory for the one request instead and sends nothing to the browser but the finished credential; a lost answer there leaves at most one unused credential that lapses by itself.

Remembered client trust

Clients trust each server's CA once, by comparing its fingerprint or with a CA file, and keep that trust per host name and port outside every workspace; how to approve, inspect and replace it is in trust a server. The rules that make it safe:

  • The first connection is a TLS-only discovery handshake that sends no Scribe frame and no credential. After approval is saved, the client reconnects and verifies the host name, validity, chain and handshake signature with that CA before it presents a certificate or sends a password.
  • A fingerprint is the full BLAKE3 digest of the CA certificate's DER bytes, written BLAKE3:…. It is public, so a CI variable can carry it (--trust-fingerprint). Scripts and --json never prompt.
  • Records hold public CA certificates only. A corrupt record fails closed and is never silently replaced. Auth and storage endpoints have separate trust, and a credential file's auth CA is authoritative for its holder, so storage servers, gateways, administrators and machines reach auth without remembered trust.

CA rotation and handover

A saved record can hold several CAs for one server. A rotation introduces the new CA with a handover (cross) certificate: the new CA's name and public key, marked as a CA and signed by the old CA. When a verified chain passes through such a certificate issued by a CA the client already trusts for that server, the client saves the new CA beside the old one without asking: nothing is believed that the trusted CA did not vouch for. The first connection that verifies through the new CA but no longer through the old one drops the old CA, so a retired (possibly compromised) CA stops being trusted. Any other change is refused before a credential is presented, and saved trust is left unchanged until the user confirms the new fingerprint with scribe trust replace (when a server's CA changes).

scribe-ctl renew --rotate-ca storage|auth (rotate a CA) moves every server of the CA to the new one in one run, so every server behind an endpoint presents the handover before a client can drop the old CA; before the retirement each server presents [certificate, handover certificate], after it [certificate, new CA], so the fingerprint clients then see is the new CA's. The CAs' path length that lets strict validators accept that chain is explained in the four CAs.

The storage executable also provides users and permissions using the same administrator credential file. They are conveniences for auth administration, independent of the daemon's server credential.

Rollback protection

A copy of an auth data directory taken before a revocation still lists the revoked credential as live. Bringing such a copy back by copying files would silently revive it. Auth therefore keeps rollback state, and both auth and every storage server refuse to go backwards:

  • Every auth database has an incarnation, a random 128-bit ID created by init, migrate and restore, and a revision counter. The revision moves in the same write transaction as every applied administrative change (including credential revocation, a disabled user, a password reset, a revoked or downgraded grant, namespace role or delegate, visibility changes and a disabled server) and every logout. Every SAU2 answer names both.
  • serve --revision-file FILE names the revision witness, a small file holding <incarnation> <revision>. init creates it; migrate and restore write it. The service rewrites it atomically after every change that moves the revision. Keep it, like the pepper, outside the auth data directory and out of that directory's backups: a backup must not carry its own witness. Startup refuses a database behind its witness (auth database is at revision R but its witness records W: it was restored from an older copy, which would revive revoked credentials; run scribe-auth restore instead) and a witness of another incarnation. A database ahead of its witness (the service stopped between committing and rewriting it) simply rewrites the witness.
  • Every storage server durably records the newest incarnation and revision it has seen in its auth-state.redb before acting on a newer answer, and refuses every answer from a lower revision of the same incarnation with auth_unavailable: "the auth service is behind a revision this server already saw: its data was restored from an older copy, which would revive revoked credentials; restore it with scribe-auth restore". It logs auth.rollback_detected. A new incarnation is accepted and logged as auth.incarnation_changed; the incarnation it replaced is retired, and any later answer from a retired incarnation, at whatever revision, is refused the same way (the pre-restore copy has come back). The most recent 256 retired incarnations are kept.

The only way to bring an older copy back is scribe-auth restore, which starts a new incarnation, moves the revision past both the backup and the witness, and revokes every credential issued before it; see auth backup and restore.

Administration and retries

For step-by-step replacement and lost-credential procedures, see certificates and credentials.

scribe-auth access shows who can do what in one report, for a single storage server or a whole cluster alike: every account with its kind (person or service) and state, and each thing it may do, one row each: auth administration, namespace administration, reading or delegation, repository roles, live credentials and signed-in sessions. --user NAME narrows it to one account. It is built from users list, grants list, namespace-admins list, namespace-readers list, delegates list and credentials list, read with the administrator credential, and changes nothing; see the CLI reference for its columns.

users enable/disable, servers enable/disable, credentials revoke ID, grants revoke USER REPOSITORY, namespace-readers set/revoke, namespace-admins set/revoke, and delegates set/revoke provide the corresponding lifecycle operations. users add --administrator creates another central administrator; users add --service creates a service account. credentials list shows credential IDs, kinds, subjects, labels, key IDs and expiry and creation times, never keys; credentials list --user NAME shows one account's sessions and credentials from a per-account index rather than a scan. Revoked credentials stay listed as disabled until a day after their expiry, when the expiry sweep removes them. Disabling a server blocks its credentials while it is disabled; enabling it again makes them usable. Disabling a user revokes its credentials permanently.

Two limits keep administration and each account bounded. credentials revoke refuses the last live (enabled, unexpired) administrator credential of an enabled auth administrator with the last administrator credential cannot be revoked; issue another first: sessions cannot administer, so issue a replacement with credentials create before revoking the old one, or disable the account instead. Credentials from administrator sign-in are never guarded this way and never count as the replacement. Each account holds at most 256 live credentials (sessions, automation, administrator and service-account credentials together); a new one beyond that is refused with LIMIT_EXCEEDED until unused ones are revoked or expire. Revoked and expired credentials do not count. The auth service also enforces credential lifetimes itself: administrator and server credentials at most 3650 days from issuance, service-account and automation credentials at most 365; only the bootstrap credential from init never expires. Expired credentials are removed by the running service itself: a bounded sweep every 15 minutes deletes credentials that expired more than a day ago, with their key and index rows (the grace keeps a slightly late client hearing "expired", not "invalid"), and logs credentials.pruned. There is no cleanup command to schedule or forget; audit rows and operation outcomes are never pruned.

Offline administrator recovery

When every administrator credential is lost or expired, scribe-auth admin-credential issue issues a short-lived one directly into the stopped auth data directory for an existing, enabled person administrator, under the directory's lock and in one durable transaction with its audit row (admin-credential-issue-offline, labelled offline-recovery). It never creates an administrator or changes an account. The procedure is in no administrator credential left.

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 audit rows with timestamp, immutable actor ID, command, operation ID, status, and the request's arguments. Arguments never include a password; a public key is shown as key:<key ID>. Besides every administrative request (applied or rejected), the audit log records each login, logout, automation credential creation and derivation, offline administrator issuance and restore. Exact request fingerprints and outcomes are kept in the operation table.

Administrative mutations persist their operation ID and exact request, and for credentials create the new device key, in the state directory before transmission: auth-admin under the Scribe configuration root (the same root scribe --config-dir / SCRIBE_CONFIG_DIR selects), or --state-dir. The directory is created private and may not be inside .scribe. Unknown outcomes retain the pending files. 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; another administrator's operation ID is answered like an unknown one. Confirmed outcomes remove the local pending request. Local administrative invocations use an OS lock to avoid racing credential files or pending requests.

Sessions, revocation, and failures

Storage caches each positive or negative authorization decision per credential and repository for at most 30 seconds, capped by the caller's credential expiry, a delegating gateway's credential expiry, and the server credential's 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, within the configured storage connection cap.

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 credential, or changing a grant affects new operation admissions within 30 seconds, whatever the lifetime of the certificates involved. 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, keeping the pepper, the revision witness and credential files separately.

Before any backend work, a storage server or gateway decides from the verified certificate how much the caller may hold: a source address may hold only a few connections that have not finished their handshake (the oldest is closed for a newcomer when the table is full), one principal may hold at most max_connections_per_principal connections, and all of one principal's request bodies together may reserve at most a quarter of the listener's request-body budget. The principal is the user ID, also for traffic a gateway delegates.

Each secure control session returns a random 256-bit binding secret. Up to four additional data connections must present the same credentials, certified for the same keys, prove the binding secret, and reference a live control session on that server. Closing the control connection stops further requests on its data connections. The inactivity timeout applies to the session as a whole, so a control connection is not closed for silence while one of its data connections is transferring.

Protocol 4 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 same file records the newest auth incarnation and revision the server has seen (see rollback protection).

Both layers claim the same normalized request: the principal's display name replaces the client's author before the operation claim is written, and attribution is not part of the operation fingerprint. Renaming an account at the auth service therefore never turns the retry of a submit whose reply was lost into operation_intent_mismatch; it still resolves to the original outcome.

Workspace registration is a Reader operation that writes one durable row per workspace ID, so each account may register at most 256 workspaces per repository ([limits].max_workspaces_per_principal). Registering an ID the account already owns is idempotent and never consumes another. Because an ownership claim is permanent, an account may also make at most 64 new workspace claims per hour on one server. Both refusals are limit_exceeded.

The authenticated user supplies submit 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.

Developer reference: auth protocol and storage

SAU2 frames are "SAU2", a little-endian u32 payload length (at most 1 MiB), and the payload. A request is a 16-byte operation ID, a u16 count of str16 arguments (the verb first; at most 130 strings of at most 32,768 bytes, since certificates travel as PEM text), and a u8 flag followed by one str16 secret. The secret is the password of login / login-outcome and admin-login / admin-login-outcome (1–1024 bytes) or the new password of users-password (12–1024 bytes); any other verb carrying one, or these verbs without one, is malformed. A successful reply is u16 0, the 16-byte auth incarnation, a u64 revision, and at most 256 rows of strings; a failure is a u16 code and a message. The caller's identity comes only from the TLS client certificate: its SPIFFE ID and the digest of its public key, checked against the credential rows. A connection may carry any number of sequential requests. The full verb table is in the wire specification; the records are in the on-disk format.

Login arguments are [login, username, credential_id, public_key_hex, label] with a nonzero persisted operation ID; the public key is the device key's SubjectPublicKeyInfo DER in lowercase hex. A successful login or known outcome returns [["ok", expiry_unix_ms, credential_certificate_pem]]; an unknown outcome returns no rows. The login intent hashes canonical encode_intent bytes with a zero operation ID and the four arguments after the verb; no password bytes are hashed into it. admin-login / admin-login-outcome take the same arguments and answer the same rows; their intent hashes the string admin-login followed by those four arguments, and the recorded outcome shares login's sessions/OPERATION key, so one ID never serves both. logout / session-outcome take only the verb and act on the credential that presented the certificate: [["ok"]], and an already revoked, expired or pruned credential also answers ok without writing. users-password arguments are [users-password, username, random_generation_id], followed by the new password as the secret. The durable intent uses canonical encode_intent with a zero operation ID and all three arguments, followed by password bytes, keyed using the pepper and context Scribe auth password operation intent v1. The administrator's local commitment uses a key derived from the device key with context Scribe auth admin password commitment v2. Both keys use BLAKE3's derive_key. Password changes use the durable administrative-outcome path; replay cannot replace the original password.

The SCRIBE_AUTH marker is scribe-auth\nschema=4\nprotocol=2\npassword=argon2id-pepper-v1\n. Any other schema is refused at open; only scribe-auth migrate converts schema 3. Its crash windows (auth-migrate-bundle-durable, auth-migrate-records-durable, auth-migrate-marker-staged) are exercised through the fault-injection feature and SCRIBE_FAULT, as is the window of offline administrator issuance (auth-admin-credential-secret-durable: credential file durable, credential not yet committed).

auth_v1 stores rows using the single-row SAU2 result encoding. The account, grant and policy rows are:

KeyRow
meta/pepper-idfull BLAKE3 fingerprint of the raw 32-byte pepper
meta/trust-domain, meta/incarnation, meta/revisionSPIFFE trust domain; rollback state
ca/credential/NNNN, ca/access/NNNNCA certificate PEM and PKCS#8 key; the highest number signs, all are trusted
users/USERNAMEimmutable ID, username, enabled, administrator, kind (person or service)
user-ids/IDusername
passwords/USERNAMEpeppered Argon2id PHC string, random 128-bit generation as lowercase hex
servers/NAME, server-ids/IDimmutable ID, name, enabled, namespace; name by ID
credentials/CREDkind, subject, expiry Unix ms, enabled, label, key ID, algorithm, creation Unix ms, parent, password generation
credential-keys/KEY_IDcredential ID (a key is registered once)
credential-certs/CREDthe credential certificate, replayed by outcome queries
user-credentials/USERNAME/CRED, server-credentials/NAME/CREDcreation Unix ms (per-account and per-server index)
meta/audit-sequence, audit/SEQUENCE (20 digits, zero-padded)Unix ms, actor user ID, command, operation ID, ok or rejected, arguments
grants/NAMESPACE/REPOSITORY/USER_IDrole
namespace-admins/NAMESPACE/USER_ID, namespace-readers/NAMESPACE/USER_ID, delegates/NAMESPACE/USER_IDusername

User rows decode with exactly five fields, strict true/false flags, and a known kind; a service row with the administrator flag set is corrupt. Each credential kind requires its subject's kind: user, session, and automation* belong to persons, service-reader/service-writer to service accounts, server to a registered server, and a mismatch is refused as credential does not match its account. Credential rows decode with exactly ten fields, a known kind, the p256 algorithm, a canonical key ID, a parent only on derived automation credentials, a password generation always on sessions and automation credentials, optionally on user credentials (exactly those administrator sign-in issued), and never on service or server credentials. Anything else fails the request (and the expiry sweep) as corrupt instead of being skipped or read as a default.

Visibility policy is stored as visibility/<namespace>/<repository-id> with a canonical auth row ["private" | "public", decimal_revision]. Missing means private at revision zero. visibility-get is an administrator read. visibility-set arguments are [command, repository, visibility, expected_revision] and use the ordinary durable administrator intent/outcome/audit transaction.

authorize arguments are [authorize, credential_id, key_id, repository, delegate_credential_id, delegate_key_id], the delegate pair empty when the caller connected directly; it returns [principal_id, name, role, namespace_admin, lease_ms, public_read, namespace_reader], and a row of any other length is refused as malformed. The caller's credential must be live and registered for that key; a delegate must be a live service-account credential whose account holds delegates/NAMESPACE/USER_ID for the server's namespace. namespace_reader says the credential reads the whole namespace, whatever key was asked: a namespace-readers row, or, for a person's credential, namespace administration. A service account's credential gets it only from its own namespace-readers row. For a nonempty key, the account's live role is the stronger of its direct grant and Reader when namespace-readers/NAMESPACE/USER_ID exists; the credential's ceiling then applies: sessions, administrator credentials, and automation-admin keep it (and namespace administration); automation/automation-write/automation-maintain cap it as described below with no namespace administration; service-reader caps it at reader and service-writer at writer, never with namespace administration, even if a namespace-admins row exists. authorize-batch reports read access from the same decision.

Administrative argument vectors for accounts: users-add is [command, username, id, administrator, kind]; users-list rows are [users/NAME, id, name, enabled, administrator, kind]; credentials-add is [command, kind, subject, credential_id, public_key_hex, expiry_ms, label] with kind user, server, service-reader, or service-writer, and returns the credential certificate; credentials-certificate returns it again for a credential ID; servers-add is [command, name, namespace, id]; namespace-readers-set / -revoke and delegates-set / -revoke are [command, username, namespace]; namespace-readers-list and delegates-list are [command, cursor, namespace?]. public-policy takes a repository key and returns [visibility, revision, lease_ms]. authorize-batch takes a caller credential and key ID and a delegate pair (all four empty means anonymous) followed by 1–125 repository keys and returns one [can_read, lease_ms] row per key. All three require a storage server's credential certificate and refuse keys outside that server's namespace. The batch validates nonempty caller credentials even for public repositories.

Web automation credentials

The web account settings issue separate personal API tokens (/settings/api) and MCP tokens (/settings/mcp). These are web-level bearer tokens for the web's HTTP API only; no Scribe service accepts them. Both are directly usable in Authorization: Bearer …; no access-token exchange is required. scribe_pat_<id>_<secret> is accepted only on opted-in REST routes; scribe_mcp_<id>_<secret> is accepted only on /api/v1/mcp. The ID is a random 128-bit lookup identifier, and the secret is a separate random 256-bit value. The web database stores a SHA-256 digest of the entire public token, never its recoverable secret. Each web token is bound to a separate Scribe automation credential: a device key the web API generated, sealed in the web database, and certified by auth. Neither is embedded in a URL, logged, or returned from listing.

Creation requires a live browser password session. The API persists the new key and operation ID before calling auth (automation-create, presented with the browser session's own credential certificate), checks the operation outcome before issuance, and only activates the web token after successful durable issuance. Failed or uncertain issuance remains denied and queued for upstream revocation. Tokens have named read scopes (repositories:read, contents:read, history:read), explicitly selected write scopes such as contents:write, and a fixed 7, 30, 90, or 365 day expiry. The upstream credential's label records the token name and scopes (web-api ci: flows:read, runners:write) so scribe-auth credentials list explains each ceiling. There are at most 100 active credentials per account, enforced transactionally per owner in SurrealDB. Lists return the latest 200 records, with active records first.

Auth's automation (reader), automation-write (writer), automation-maintain (maintainer), and automation-admin (namespace-administration ceiling) credential kinds are bound to the issuing user and password generation. Only a password session can create one, over its own mutual-TLS connection, so nothing a storage server or gateway ever sees can mint one; the audit row records the creating session. Each survives logout of the issuing browser session, but user disable, password reset, expiry, or explicit revocation invalidates it. It cannot create further automation credentials or perform auth administration; it can only have auth certify a runner's key as its derived child (below). Ordinary automation credentials remain capped to reader, writer, or maintainer without namespace-administrator privileges. The separately selected repositories:create scope requests namespace-admin delegation: namespace privileges apply only where the issuing user has a current Scribe namespace-admin grant. Removing that grant removes provisioning access. This still permits neither auth administration nor further delegation. Existing read-only credentials remain read-only; only new credentials explicitly issued with the writer ceiling can write. A writer credential never elevates a reader grant and cannot edit maintainer-only repository configuration. Storage continues checking current grants. The web middleware also checks live identity on every automation request, including MCP discovery.

A runner (runners:write, reader ceiling) never receives the web token's credential. At check-in it sends the public half of its own device key; the web API derives a child credential for that key from the token's automation credential (automation-derive: same kind, subject, ceiling and expiry, revoked with its parent) and returns only its certificate, so a build machine checks repositories out as the same read-only identity without any private key leaving it. See Flows.

Revocation denies new web requests as soon as the database update commits; the background worker settles upstream revocation with its persisted operation ID. Already-admitted operations may finish; a durable commit is never cancelled. Pending creations are left undisturbed for two minutes before cleanup. Auth's normal bounded permission cache can briefly retain an already-issued authorization; a token never extends that cache lifetime. No privileged service credential or user password is stored for automation.

Remote collaboration administration

Workspace administration reuses repository roles: workspace list shows the current principal's registrations, --everyone requires Maintainer, and workspace remove permits an owner with read access or a Maintainer for another owner. --all-repos --everyone is available to namespace administrators across their accessible repositories, including through gateways. Active workspace quotas are reclaimed by retirement; ownership claims and past operation outcomes remain permanent. Forced unlock with --repo requires Maintainer, even for the caller's own lost workspace, and works without a local working copy. Anonymous public readers cannot list or retire registrations.

When auth is disabled, every connected client can list all registrations, retire them, and force-unlock. A gateway preserves that policy; adding one does not add authentication. See the deployment permission table for direct and gateway setups with and without auth.

These are storage operations under existing grants. Auth-administrator credentials still have no implicit storage access. Server-local doctor, backup/restore, and repair remain protected by data-root access and the exclusive OS lock.

Zdroj docs/auth.mdVerze 2b8dcccb