# DWH REST Per-Installation Authentication — Design **Date:** 2026-08-20 **Status:** Approved by the owner ## Purpose Replace the single static `X-API-Key` currently protecting the PSD `/dwh/` route with a reusable, server-side authentication component that assigns one independently revocable credential to each ThothII installation. The design must preserve the existing portable connector contract. A ThothII installation on macOS, Windows, or Linux continues to send `X-API-Key` over HTTPS when its selected transport is `rest_api`. Installations using `postgres_direct` or `ssh_tunnel` are unaffected. The component belongs in the ThothII source repository so that it can be reused on another DWH server, but it is optional server infrastructure. It is not part of the portable application runtime and is not started or stopped by `tht`. ## Confirmed context - The exposed credential must be considered compromised and must eventually be revoked. - The current PSD route accepts one shared static `X-API-Key` through Nginx `auth_request`. - The Mac installation must continue to access PSD through REST. - Future remote ThothII installations will also use REST and cannot depend on an SSH tunnel. - The new ThothII installation on the PSD server will use `postgres_direct` with a dedicated, demonstrably read-only DWH role. - The current self-issued TLS certificate remains in use. Clients that do not already trust its issuer require a separately delivered `TLS_CA_FILE` and must verify the approved fingerprint. - There is no separate test environment. This is accepted and is not itself a blocker. - Existing ThothII sessions, derived indexes, and model caches are test data. They are not migrated or protected by session-specific backup work. - The legacy stack remains unchanged until the deployment program reaches its explicit stop gate. This is an authorization boundary, not a session-preservation requirement. ## Scope This design includes: - the reusable `dwh-auth` service and its administrative command; - a protected file-based credential registry; - Nginx `auth_request` integration; - one credential identity per ThothII installation; - creation, delivery, rotation, optional expiry, and revocation; - a dual-key transition from the exposed shared credential; - generic and PSD-specific operator documentation; - automated tests and production-safe acceptance checks. This design does not: - change the ThothII REST header or connector protocol; - add DWH credential administration to the portable `tht` CLI; - place server credentials in a workspace repository; - replace PostgreSQL grants, RLS, or the PostgREST read-only role boundary; - preserve or migrate legacy ThothII sessions, Qdrant indexes, or Ollama caches; - change the current certificate or resolve the separate `.it` versus `.com` public-origin issue; - authorize any current server, Nginx, database, or service mutation. ## Alternatives considered ### Extend the portable `tht` CLI This would provide a single operator command surface, but it would distribute server-only DWH administration code to every macOS and Windows installation. The DWH route also has a lifecycle independent from the local ThothII application. This option is rejected. ### Maintain a manual Nginx key map This is initially small, but it encourages plaintext credentials in Nginx configuration and makes atomic rotation, redacted inventory, and reliable revocation harder. This option is rejected. ### Use SQLite SQLite is not a declared project dependency and is unnecessary for the expected registry size and write rate. Introducing a database would add packaging, migration, backup, and corruption-recovery work without improving the required contract. This option is rejected. ### Selected approach Add a standalone, standard-library Go component with a protected file registry. On PSD it runs as an independent `systemd` service and communicates with host Nginx through a Unix socket. ## Component and lifecycle boundary The repository will contain a separate component, expected under `tools/dwh-auth/`, plus generic deployment templates and documentation. Its build and tests are isolated from the portable application entrypoints. On PSD: - `dwh-auth` runs under a dedicated, unprivileged system account; - `systemd` owns its lifecycle; - the service is not included in the canonical ThothII Compose stack; - the registry resides outside both the Git checkout and ThothII application data; - the service receives read-only access to active credential records; - Nginx is the only runtime caller and reaches it through a permission-restricted Unix socket; - stopping, updating, or replacing ThothII does not interrupt `/dwh/` authentication. The same source may be deployed on another Linux DWH server. Platform-specific service packaging beyond the approved PSD `systemd` deployment is not required by this implementation. ## Credential model One credential identifies one ThothII installation, not one human user. An installation may have more than one temporarily active generation during rotation. The external key contains a version marker, a public key ID, and a random secret: ```text thtdwh_v1.. ``` Requirements: - the public key ID is generated by the tool and contains only a strict safe alphabet; - the secret is generated with the operating system cryptographic random source; - the secret has at least 256 bits of entropy and uses an unambiguous transport-safe encoding; - the complete header has a fixed maximum length; - installation IDs are operator-provided, non-secret, unique labels and must not contain personal or clinical information; - descriptions are optional non-secret operator metadata. Because the secret is a uniformly random 256-bit value rather than a human password, the registry stores a SHA-256 digest and verifies it with a constant-time comparison. No password hashing or external cryptographic runtime dependency is needed. ## Protected file registry The registry has versioned active and revoked records: ```text / active/.json revoked/.json ``` An active record contains: - schema version; - public key ID; - installation ID and optional description; - encoded SHA-256 digest; - creation timestamp; - optional expiry timestamp, absent by default. A revoked record additionally contains the revocation timestamp and a non-secret reason. The original key never appears in either directory. Multiple key IDs may refer to the same installation ID during a rotation. Creation uses an exclusive temporary file, file flush, directory-safe atomic rename, restrictive ownership and mode checks, and refusal of symlinks or unsafe paths. Revocation atomically moves a record from `active` to `revoked` on the same filesystem. A malformed or unsafe record never authenticates. The runtime service does not update registry files. Successful and failed use is recorded only in the sanitized service journal, avoiding per-request writes and keeping the service's registry access read-only. ## Administrative interface The standalone binary provides a server-only administrative surface equivalent to: ```text dwh-auth key create --installation-id ID --description TEXT --output ABSOLUTE_FILE dwh-auth key import --installation-id ID --from-file ABSOLUTE_FILE dwh-auth key list [--json] dwh-auth key revoke --key-id ID --reason TEXT dwh-auth key status --key-id ID [--json] dwh-auth check ``` The exact syntax will be frozen in the implementation plan and tests. The interface must obey these invariants: - secrets are never accepted as command-line values; - `create` writes the generated credential once to a new absolute file with restrictive access; - `import` reads an existing protected file without displaying its content; - normal stdout contains only non-secret identifiers, abbreviated fingerprints, status, and paths; - JSON output is pristine and contains no credential value or digest; - an existing output file is never overwritten; - listing and status commands never expose digests; - errors and logs pass explicit secret-leak regression tests. ## Provisioning and delivery The operator creates one key for each installation and transfers it through an approved protected channel, such as an organizational password manager, a secret manager or MDM, or authenticated file transfer when the recipient has suitable access. Email, ordinary chat, and ticket bodies are not approved delivery channels. On the recipient installation: - the normal interactive path saves the value through authenticated Workspace management, where ThothII keeps it in the installation-local encrypted workspace vault; - a supported headless deployment may place it in a protected file referenced by `API_KEY_FILE`; - the credential never enters the shared workspace repository or ordinary environment values; - the private CA is delivered separately and configured with `TLS_CA_FILE` when the certificate chain is not already trusted; - the operator verifies the documented certificate fingerprint before trusting the CA file. After configuration, the recipient runs the existing workspace connection test against the harmless `/rpc/ping` diagnostic. Only after the server and recipient both confirm the public key ID may the one-time delivery copy be removed according to the organization's secret-handling procedure. ## Expiry and rotation Keys have no automatic expiry by default. This avoids unplanned outages for remote installations that may not be continuously administered. An optional expiry timestamp is supported for sites whose policy requires it; an expired key is rejected without fallback. Administrative status output warns about approaching expiries without exposing secrets. Normal zero-downtime rotation is: 1. create a second key generation for the installation; 2. deliver and configure it on the client; 3. verify `/rpc/ping` and the sanitized server key ID; 4. revoke the earlier key; 5. prove that the revoked key receives `401`; 6. remove temporary delivery material after recipient confirmation. ## Request flow and Nginx contract ```text remote ThothII -> HTTPS /dwh/ with X-API-Key -> Nginx auth_request subrequest -> dwh-auth over a Unix socket -> valid: Nginx proxies the original request to internal PostgREST -> invalid or revoked: Nginx returns 401 -> authenticator fault: Nginx returns 503 ``` `dwh-auth` authenticates only. It does not proxy the original request, read a clinical response, or connect to PostgreSQL. PostgREST remains inaccessible as a public direct backend. Nginx integration must: - forward only the bounded credential header to the internal verification endpoint; - suppress the original request body in the auth subrequest; - discard client-supplied identity or audit headers; - accept only the authenticator's success result; - return the same generic `401` for missing, malformed, unknown, expired, and revoked keys; - map authenticator or registry faults to `503`, never to permissive access; - apply bounded failure-rate protection; - log only timestamp, result, request metadata already approved for the route, and the verified public key ID; - preserve the existing PostgREST path and response contract for successful requests. The authenticator returns a verified public key ID only after successful authentication. Nginx may use that value in its sanitized access log but does not need to forward it to PostgREST. ## Fail-closed behavior The following conditions deny access: - header absent, duplicated, oversized, malformed, or encoded incorrectly; - unknown public key ID; - digest mismatch; - revoked or expired record; - unsafe permissions, symlink, malformed JSON, schema mismatch, or inconsistent record identity; - unreadable registry; - service startup or runtime failure. Credential failures return a generic `401`. Infrastructure failures return `503` after the Nginx mapping is applied. Neither response reveals whether an installation ID exists. Health reporting is local-only and does not weaken the authentication decision. ## PSD dual-key transition The exposed shared credential is represented temporarily as `legacy-shared`. It is imported only from an approved protected source and is never placed in a command argument, terminal output, document, or evidence file. Because the existing value does not use the new versioned key format, it is stored as the only permitted `legacy_raw` record. The service compares the digest of the complete opaque legacy header only for that reserved record. After `legacy-shared` is revoked, no unversioned credential is accepted. The production route does not switch to the new authenticator until all of the following hold: 1. protected backup and exact rollback instructions exist for the affected Nginx and service configuration; 2. the new service passes local synthetic checks while disconnected from the public route; 3. `legacy-shared` passes `/rpc/ping` through the candidate authentication path; 4. a new per-installation Mac key passes the same diagnostic; 5. a random invalid key is rejected; 6. `nginx -t` passes and the authorized operator approves reload. After the Mac uses its new credential successfully for the agreed observation window, `legacy-shared` is revoked. Acceptance requires a positive test with the new key and a negative test proving `401` with the legacy key. The new secret must not appear in service, Nginx, shell, or application logs. Rollback during the dual-key window restores only the reviewed authentication route and service configuration needed to keep REST clients working. It does not preserve or restore legacy ThothII sessions, indexes, or model caches. ## PostgreSQL authorization boundary Successful API-key authentication is not proof of read-only database authorization. The existing PostgREST role, exposed schemas, function privileges, default privileges, and direct network reachability remain subject to the catalog-only survey activity. Project execution cannot treat `dwh-auth` as a substitute for a demonstrably read-only `dwh_reader` boundary. ## Verification strategy Automated tests use synthetic credentials and no clinical data. They cover: - key generation entropy and syntax; - create, import, list, status, optional expiry, rotation, and revocation; - constant-time digest verification; - missing, duplicate, malformed, oversized, unknown, expired, and revoked credentials; - corrupt records, unsafe modes, symlinks, path traversal, and partial files; - atomic creation and revocation under concurrent reads; - clean failure when the registry or socket is unavailable; - absence of credential values and digests in human output, JSON, errors, and logs; - Nginx `auth_request` integration against a synthetic upstream and `/rpc/ping` fixture; - `401` for credential failures and `503` for infrastructure failures; - unchanged successful request path and response body; - independent service lifecycle from the ThothII stack. Portability regression checks prove that: - the existing REST connector still sends the same `X-API-Key` header; - `postgres_direct` and `ssh_tunnel` configuration remain unchanged; - canonical local and server ThothII Compose rendering does not acquire `dwh-auth`; - macOS and Windows installation contracts do not require the new binary or files; - existing `tht`, backend, frontend, and harness gates remain green in proportion to the touched source boundaries. There is no separate PSD test environment. Local synthetic and integration tests precede a bounded, reversible production rollout using only `/rpc/ping`. The absence of a test environment does not trigger preservation work for legacy test sessions. ## Documentation deliverables Implementation is incomplete until the following clear, secret-free documents exist and pass their documentation checks: 1. a generic server installation and administration guide for `dwh-auth`; 2. a generic enrollment guide for issuing a key to a macOS, Windows, Linux, or headless ThothII installation; 3. exact GUI and `API_KEY_FILE` configuration paths; 4. a TLS guide covering `TLS_CA_FILE`, trusted delivery, fingerprint verification, renewal, and coordinated client updates; 5. a PSD runbook for backup, installation, legacy import, dual-key activation, Mac update, observation, revocation, negative testing, and rollback; 6. a troubleshooting table for `401`, `503`, TLS failures, revoked keys, and unreadable registry records; 7. sanitized evidence templates that never request secret values or digests; 8. links from the existing local, server, workspace, and final unified user manuals. Examples use synthetic hostnames, IDs, fingerprints, and credentials. No real key, certificate body, password, connection string, or clinical result is committed. ## Acceptance criteria The design is implemented only when: - each REST installation can be identified and revoked independently; - no plaintext server credential is stored in the registry; - the portable ThothII connector contract is unchanged; - the PSD ThothII server continues to select `postgres_direct`; - stopping ThothII does not stop `dwh-auth` or `/dwh/` authentication; - the authenticator fails closed and its logs are sanitized; - the dual-key production procedure proves both positive and negative outcomes; - the exposed legacy credential is demonstrably rejected; - the current TLS limitations are documented precisely rather than silently bypassed; - database read-only authorization is independently verified; - all documentation deliverables are reviewed and reproducible by an operator who did not write the implementation. ## Authorization gates Approval of this design authorizes documentation and planning only. It does not authorize: - reading or importing the legacy credential; - creating real keys; - installing a binary or `systemd` unit; - writing the registry directory; - modifying or reloading Nginx; - querying or changing PostgreSQL; - changing, stopping, or removing the legacy ThothII stack. Those actions require the executable plan, the remaining survey gates, explicit mutation authorization, and retained sanitized evidence.