From a21e2c156d5eb007b5d50921209a7825a5bb29a5 Mon Sep 17 00:00:00 2001 From: User Date: Fri, 21 Aug 2026 20:23:29 +0200 Subject: [PATCH] docs: design Project A auth runtime projection --- ...a-server-auth-runtime-projection-design.md | 424 ++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-21-project-a-server-auth-runtime-projection-design.md diff --git a/docs/superpowers/specs/2026-08-21-project-a-server-auth-runtime-projection-design.md b/docs/superpowers/specs/2026-08-21-project-a-server-auth-runtime-projection-design.md new file mode 100644 index 00000000..0f261100 --- /dev/null +++ b/docs/superpowers/specs/2026-08-21-project-a-server-auth-runtime-projection-design.md @@ -0,0 +1,424 @@ +# 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.