# Project A Server Authentication Runtime Projection Design **Date:** 2026-08-21 **Status:** approved **Scope:** Project A server-only authentication storage and publication **Source baseline:** `042af932ee7b686e8435aa1c5857a12c225e03c3` ## 1. Purpose Project A must use local ThothII authentication without creating a host account for the image's numeric identity `10001:10001`. The host operator runs `tht` through `sudo`, while the core runs in the container as `10001:10001`. Current code requires the authentication directory and files to be owned by the effective UID of the reader. A single physical directory therefore cannot be both the root-operated host store and the container-readable runtime store. This design separates one authoritative, root-only canonical store from a read-only runtime projection. Every successful `tht auth` mutation publishes and verifies a complete runtime generation automatically. The core never reads the canonical store and never observes a mixed `auth.yaml`/`users.yaml` pair. The change is opt-in and server-only. Existing Mac, Windows, and local Linux installations without a runtime projection retain their current files, CLI grammar, ownership rules, and behavior. ## 2. Non-goals and authorization boundary This source change does not: - start Project A or create any Project A container, network, or volume; - modify or reload Nginx; - stop, alter, or remove the legacy ThothII stack; - change DWH, ETL, Supabase, Authentik, Aritmolab, `dwh-auth`, or another shared service; - create a host user or group for numeric identity `10001`; - migrate legacy sessions, Qdrant data, Ollama data, or application settings; - expose passwords, password hashes, complete user records, or authentication YAML in logs. Implementation and automated verification use only the isolated source worktree and temporary fixtures. Applying the new contract to `/srv/thothii` remains a separate, explicitly authorized pre-start operation. ## 3. Installation contract The installation descriptor gains an optional nested runtime projection: ```yaml profile: server authentication: configDirectory: /srv/thothii/operator/auth runtimeProjection: directory: /srv/thothii/secrets/auth-runtime uid: 10001 gid: 10001 ``` `authentication.configDirectory` remains the canonical store and must equal `THT_AUTH_CONFIG_ROOT`. When `runtimeProjection` is present: - the installation profile must be `server`; - the host platform must be Linux for mutation and publication operations; - `directory` must be an absolute canonical path distinct from `configDirectory`; - `uid` and `gid` must both equal decimal `10001`; - `directory` must equal `THT_AUTH_RUNTIME_ROOT` from the installation environment; - `sudo tht` is the only supported writer and publisher; - failure to establish effective UID 0 before a mutation or publication fails before changing canonical or runtime state. Projected server `tht setup`, including `--configure-only`, must also run with effective UID 0. It creates the canonical root and files directly as `root:root 0700/0600`; it must not first create an operator-owned store that a later `sudo tht` invocation cannot validate. When `runtimeProjection` is absent, `THT_AUTH_RUNTIME_ROOT` must also be absent and the existing single-directory behavior remains unchanged. The descriptor exposes separate accessors for the canonical authentication directory and the optional runtime-projection specification. Generic host validation continues to validate the canonical store against the effective host UID. Pre-start commands for a projected server additionally require a ready runtime projection consistent with the canonical revision. ## 4. Filesystem model ### 4.1 Canonical store The authoritative store remains compatible with the existing host auth implementation: ```text / auth.yaml users.yaml # local mode only .auth.lock .auth-transaction.lock ``` The directory is `root:root 0700`; all regular files are `root:root 0600`, have link count one, and are reached only through canonical, non-symlinked paths. `auth.yaml` and `users.yaml` retain their current strict schemas. The canonical store is the only data restored from an authentication backup and the only state accepted as authority by recovery. ### 4.2 Runtime projection The projection has this stable bind-mounted root: ```text / CURRENT generations/ / manifest.json auth.yaml users.yaml # local mode only ``` The projection root, `generations`, and generation directories are `10001:10001 0700`. All regular files are `10001:10001 0600` with link count one. The privileged publisher sets these numeric IDs directly; it does not resolve or create a host account. The server Compose mount is read-only, so the core can read but cannot alter the projection even though its effective UID is `10001`. No symlink, hardlink, device, FIFO, socket, unexpected file, permissive mode, unexpected owner, or non-canonical path is accepted. Publishing uses descriptor-relative operations with no-follow checks, verifies the opened object before and after I/O, and fsyncs files and parent directories before making a generation ready. The runtime publisher and validator live in a projection-specific Linux package whose operations take explicit expected UID and GID values. The existing generic canonical `safeio` ownership rule continues to require owner equals effective UID and is not weakened to accommodate the projection. ### 4.3 Generation identity For each canonical snapshot, the publisher computes lowercase hexadecimal SHA-256 digests of the exact bounded bytes of `auth.yaml` and, in local mode, `users.yaml`. The generation ID is the lowercase SHA-256 of four UTF-8 lines joined by LF and terminated by one final LF: ```text thothii-auth-projection-v1 mode= auth=<64-lowercase-hex> users=<64-lowercase-hex-or-dash> ``` The generation directory name is the 64-character generation ID. For a projected installation, the canonical revision is exactly `sha256:`; no second revision algorithm exists. Its strict `manifest.json` contains only schema version `1`, generation ID, mode, that canonical revision, exact filenames, byte sizes, and per-file SHA-256 digests. JSON is emitted canonically with a trailing newline and no unknown or duplicate fields. Non-projected installations retain the existing revision calculation and output for compatibility. Generation files are immutable after publication. An existing directory with the expected ID is reused only after complete owner, mode, type, size, content-digest, manifest, and canonical-revision verification. Any mismatch is an integrity failure; it is never repaired in place. `auth publish` can recover a corrupt target generation only while `CURRENT` is blocked. It revalidates the exact directory identity and performs a bounded descriptor-relative deletion of the whole target without following links, then publishes a newly staged complete directory under the expected generation ID. It never edits a published generation file in place. Deletion requires the target directory and every traversed entry to remain confined, owned by `10001:10001`, and to have the exact allowed directory/file type, mode, link count, and bounded name. File digests are checked when readable but are not required to match when digest failure is the reason the whole generation is being replaced. Any structurally unsafe or unrecognized entry is not deleted automatically and keeps the command in a sanitized integrity-failure state. Publisher-only temporary names are closed and bounded: - `generations/.stage--` is the only staging-directory form; - `.current-.tmp` is the only selector temporary-file form; - no quarantine namespace exists. After acquiring the transaction lock and before new staging, `auth publish` scans only these two namespaces. A stage is automatically removed only when its transaction equals the transaction in a persisted blocked selector and the complete bounded no-follow ownership/type/mode/link validation passes. A selector temporary file may be removed for any syntactically valid transaction ID only after the same regular-file validation and strict bounded JSON decoding pass; this covers a crash before the blocked-selector rename. Entries with another name, an unrelated stage transaction, unsafe metadata, excessive contents, or an invalid document remain untouched and keep recovery blocked for explicit operator investigation. Recovery then creates a fresh transaction ID and rebuilds its stage from the canonical store. ## 5. Atomic selection and fail-closed state `CURRENT` is the only mutable runtime selector. It is a bounded, strict JSON document published by same-filesystem temporary-file creation, file fsync, atomic rename, and runtime-root fsync. A ready selector contains exactly: ```json { "version": 1, "state": "ready", "transaction": "<32-lowercase-hex>", "generation": "<64-lowercase-hex>", "previousGenerations": [""] } ``` A blocked selector contains exactly: ```json { "version": 1, "state": "blocked", "transaction": "<32-lowercase-hex>" } ``` Transaction IDs are 128 random bits generated by the operating system. Missing, unsafe, malformed, unknown-version, `blocked`, or internally inconsistent selectors make runtime authentication unavailable. Only host `tht` can determine that a structurally valid projection is stale relative to the unmounted canonical store. The backend never falls back to the canonical directory, an older generation, an environment password, or upstream authentication. The backend runtime provider reads and validates `CURRENT`, resolves only a safe generation name under `generations`, and loads a complete immutable in-memory snapshot before returning: manifest, strict authentication configuration and, in local mode, the parsed local-user registry. All declared file digests and the generation identity are verified as part of that one load. The projected local-registry resolver consumes the in-memory registry associated with the exact loaded authentication snapshot; it never reopens `users.yaml` later. A request may therefore finish against the coherent ready generation it captured immediately before a transition, even if that generation is later removed. Every later request sees either `blocked` or the new coherent generation. The provider may cache an already verified complete immutable snapshot by its storage identity. It must re-read `CURRENT` for each authentication snapshot and must not cache a `blocked` or invalid state as ready. The in-memory snapshot is not serialized, logged, or exposed through diagnostics. ## 6. Automatic publication transaction The following commands are mutations: - `auth configure --mode local|oidc`; - `auth user add`; - `auth user set-password`; - `auth user enable` and `auth user disable`; - `auth user grant` and `auth user revoke`; - `auth user logout-all`. With a runtime projection configured, each mutation performs one serialized transaction: 1. Validate descriptor, canonical and runtime parents, ownership policy, and root privilege. For initial configure or first publication, create missing roots with their exact required numeric owners and modes before any canonical credential is written. 2. Acquire `.auth-transaction.lock` in the canonical root. The lock covers the complete operation and all publication or recovery work; existing `.auth.lock` remains the inner local-registry mutation lock. 3. Load and verify the prior canonical revision when one exists. Except for initial configure, verify that the prior ready projection equals that canonical revision. 4. Atomically publish a new `blocked` selector with a fresh transaction ID. 5. Execute the existing canonical mutation and validate its complete resulting state. 6. Stage the complete runtime generation in the runtime root, set exact numeric ownership and modes, fsync it, rename it to its immutable generation ID, and verify it through the same reader used by `auth publish`. 7. Construct history as the previous ready generation followed by its prior history, de-duplicated and truncated to two entries. Atomically publish `CURRENT` as `ready` for the new generation. 8. Re-read `CURRENT` and the complete generation, compare the runtime canonical revision with the current canonical revision, and return success only on exact equality. 9. Remove generations not named by current plus its two-entry history, without following links. Cleanup occurs only after the new ready state is verified. A cleanup error is reported as an integrity failure but never rolls the selector back to an older credential state. If the canonical mutation fails and the publisher proves that canonical bytes are unchanged, it restores and verifies the previously saved ready selector. If canonical state changed or cannot be proved unchanged, `CURRENT` remains `blocked` and the command fails with a sanitized recovery instruction. If publication or final verification fails after a successful canonical mutation, `CURRENT` remains `blocked`. No mutating command reports success while canonical and runtime states differ. ## 7. CLI behavior and recovery ### 7.1 `tht auth publish` `auth publish` accepts no password and no content arguments. It acquires the transaction lock, treats the validated canonical store as authoritative, publishes `blocked`, builds or reuses the matching immutable generation, selects it as ready, verifies equality, applies bounded retention, and exits successfully only when the runtime projection is coherent. It is the supported recovery command after interruption, failed publication, runtime tampering, or restoration of a protected canonical backup. It does not recover a password, select an arbitrary old generation, or modify canonical users. ### 7.2 `tht auth status` Existing output remains compatible. For projected server installations, text and JSON output add only projection state, generation ID, canonical revision, and equality status. It never emits YAML, password hashes, user records, transaction lock contents, or environment values. ### 7.3 `tht auth check` Before invoking the existing container diagnostic, `auth check` verifies: - canonical auth validity; - runtime selector and generation integrity; - equality of canonical and projected revisions; - exact expected owners and modes. Only after these checks pass does it run the existing core diagnostic against the runtime mount. Output remains bounded and sanitized. `--json` remains pristine JSON. ### 7.4 Pre-start commands `start`, `update --check-only`, and relevant `doctor` checks fail closed when a configured runtime projection is absent, blocked, invalid, or stale. Initial `auth configure` and `auth publish` remain available to create or repair it. Read-only auth status remains available to diagnose a blocked projection without exposing secrets. ### 7.5 Backup restore A restore archive can replace canonical `auth.yaml` or `users.yaml`, so it participates in the same projection transaction. After archive preflight identifies any `authentication-configuration` entry and before the first restore mutation, `tht restore` must: 1. require projected-server root privilege and acquire `.auth-transaction.lock`; 2. validate the prior canonical and ready projection; 3. atomically select `blocked`; 4. perform the existing checkpointed restore or its checkpoint recovery; 5. validate the final canonical store, publish its generation, and verify equality before selecting `ready`. If candidate restore fails and checkpoint recovery succeeds, the recovered canonical state is published before authentication becomes ready. If restore, checkpoint recovery, publication, or verification cannot complete, `CURRENT` remains blocked. A restore archive with no canonical authentication entries retains its current lifecycle and does not block authentication. A non-projected installation retains all existing restore behavior. ## 8. Compose and backend integration The base/local Compose contract remains unchanged: local installations mount `THT_AUTH_CONFIG_ROOT` at `/run/thothii-auth` and use the existing direct-file provider. Projected server installations add a dedicated `deploy/compose.auth-runtime-projection.yaml` override after the base profile and all declared operator overrides, but before the generated current-image override. `Installation.ComposeFiles()` inserts this file automatically whenever `runtimeProjection` is declared. The descriptor loader rejects manually listing the dedicated override, listing it without a projection, or otherwise duplicating it. This ordering and automatic selection prevent an operator override from silently restoring the canonical mount. Non-projected server installations do not include the new override and retain their existing direct-file behavior. The projection override: - requires host `THT_AUTH_RUNTIME_ROOT` as the bind source; - mounts it read-only at `/run/thothii-auth` for `core` only; - sets container environment `THT_AUTH_RUNTIME_PROJECTION_ROOT` to the literal `/run/thothii-auth`; - does not mount the canonical root anywhere; - does not add authentication environment or mounts to `workspace-maintenance`. `THT_AUTH_RUNTIME_ROOT` is only the host-side Compose bind source. `THT_AUTH_RUNTIME_PROJECTION_ROOT` is only the container-side provider selector and path. When the container variable is present, backend startup and every auth snapshot use the projected provider; the inherited `THT_AUTH_CONFIG_FILE` direct path is ignored and must retain its reviewed default value. A conflicting direct-file value is a configuration error. When the container variable is absent, the existing `THT_AUTH_CONFIG_FILE` behavior is unchanged. There is no runtime fallback between the two providers. ## 9. Security properties The implementation must preserve these invariants: - root is the sole canonical writer and runtime publisher on a projected server installation; - numeric `10001:10001` is a filesystem/container identity, not a new host account; - the core receives no write-capable auth mount; - canonical password hashes are copied only into a protected runtime generation and never logged; - a stale projection cannot silently authorize after a canonical security mutation fails to publish; - a selector cannot escape the runtime root or select a partial generation; - concurrent CLI mutations are serialized across both canonical mutation and publication; - interrupted work leaves either the prior verified ready selector, when canonical state is proven unchanged, or a persistent blocked selector; - recovery always derives runtime state from the canonical store; - local and cross-platform non-projected authentication remains behaviorally compatible. ## 10. Testing strategy Development follows strict test-driven development: each behavior is introduced by a focused test that fails for the intended reason before production code changes. Required automated coverage includes: 1. Descriptor validation for valid server projection and rejection of local profile, non-Linux mutation, non-canonical/equal paths, missing environment match, and UID/GID other than `10001`. 2. Regression coverage proving descriptors without a projection retain current Mac, Windows, and local Linux behavior. 3. Secure publication primitives: exact owners/modes, link count, no-follow behavior, bounded reads, fsync/rename ordering, strict pointer/manifest schemas, and digest verification. 4. Successful initial local and OIDC publication and automatic publication after every local-user mutation. 5. Failure injection before blocking, during canonical mutation, at each stage/fsync/rename, during final verification, and during retention; expected ready restoration or persistent blocked state is asserted explicitly. 6. Concurrent `tht` processes proving one transaction at a time and no mixed canonical/runtime result; rapid successive publications prove an in-flight request finishes from its complete in-memory snapshot after the referenced generation is pruned. 7. Backend projected-provider tests for ready, blocked, absent, stale, malformed, symlinked, hardlinked, permissive, wrong-owner, altered-manifest, altered-file, and generation-switch cases. 8. Login and session tests covering ordinary/admin login, password changes, enable/disable, grant/revoke, and `logout-all` through immutable generations. 9. Compose rendering tests proving server core mounts only the runtime projection read-only and `workspace-maintenance` has no authentication mount or environment; automatic override ordering, duplicate refusal, and both host/container environment names are exact. 10. Restore tests proving authentication-bearing candidate restore, checkpoint recovery, restore failure, and publication failure cannot leave an old ready projection; archives without auth retain existing behavior. 11. Full Go tests with race detection and vet, backend Vitest plus TypeScript typecheck, Compose contract tests, documentation gates, and secret scanners using only synthetic credentials. No automated test may inspect the real `/srv/thothii` auth material, start the Project A stack, or mutate a live service. ## 11. Documentation and later operator gate The implementation updates server installation examples, local-auth documentation, Project A preflight instructions, recovery guidance, and tests that enforce those documents. The operator guide explains: - the canonical/projection distinction in plain language; - why no host account `10001` is created; - exact descriptor and environment fields; - automatic publication semantics; - the meaning of `blocked` and recovery with `sudo tht auth publish`; - backup and restoration of the canonical store only; - safe verification without printing credentials or hashes. After source implementation, review, and verification pass, applying the contract to Project A requires a new bounded pre-start authorization. That later gate will restore the existing generated auth data to canonical `root:root 0700/0600`, create the runtime projection through `sudo tht auth publish`, run read-only preflights, and stop before starting the new stack unless the owner provides separate start authorization.