diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 919e7c74..e369408e 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -61,6 +61,10 @@ `rest_api` acceptance, the 48-hour/two-ETL observation, and revocation of `legacy-shared` are mandatory before `SURVEY_GO_PROJECT_B`. Current authorization covers read-only survey and preparation only; old-stack stop and new-stack start remain forbidden. +- **Runtime-projection preparation (2026-08-22):** the source contract and hermetic gates prepare + root-only canonical authentication plus a separate `10001:10001` read-only core projection. + This is implementation preparation and test evidence only. Project A has not been started; + applying its descriptor or any server runtime root still needs explicit authorization. ### Authentication final-review fix round 2 — remediation PASS, release gates remain (2026-08-18) diff --git a/docs/install/authentication-local.md b/docs/install/authentication-local.md index 8e95f1df..c59c6290 100644 --- a/docs/install/authentication-local.md +++ b/docs/install/authentication-local.md @@ -68,3 +68,14 @@ values into tickets, logs, or evidence. Check readiness with `tht auth check`; add `--json` for the machine contract. Use `tht doctor --json` for the aggregate installation report. + +## Projected server installations + +This section applies only when a Linux `profile: server` descriptor declares a runtime projection. +The canonical authentication root stays root-owned and is the only authority. The container reads +only the separate read-only runtime projection selected by `CURRENT`; it never falls back to the +canonical files or to a previous generation. Run projected mutations and repairs through the +root-operated `tht` commands documented in the [server guide](server.md), and never edit runtime +files directly. + +Mac, Windows, and local direct-file authentication remain unchanged when the projection is absent. diff --git a/docs/install/examples/thothii-installation.server.yaml b/docs/install/examples/thothii-installation.server.yaml index ab3e2912..3072b67c 100644 --- a/docs/install/examples/thothii-installation.server.yaml +++ b/docs/install/examples/thothii-installation.server.yaml @@ -8,7 +8,13 @@ workspaceRepository: branch: main access: ssh authentication: - configDirectory: "/absolute/path/to/thothii-auth" + # Root-operated source of truth; it is never mounted into core. + configDirectory: "/srv/example/thothii/auth-canonical" + runtimeProjection: + # The only authentication bind exposed to core by the automatic override. + directory: "/srv/example/thothii/auth-runtime" + uid: 10001 + gid: 10001 overrides: - "/absolute/path/to/ThothII/deploy/compose.session-server.yaml.example" - "/absolute/path/to/ThothII/deploy/compose.git-ssh.yaml" diff --git a/docs/install/server.md b/docs/install/server.md index fce84281..ca1e67ad 100644 --- a/docs/install/server.md +++ b/docs/install/server.md @@ -88,6 +88,73 @@ Expected: the parent is `operator_uid:10001 750`; source/operator are `10001:10001 750`; backups are `0:0 700`. Re-run the empty `getent` checks after creation. Do not make `/srv/thothii` a shared application directory. +## Projected server authentication: canonical root and runtime projection + +For a server descriptor that declares `authentication.runtimeProjection`, authentication has two +different roots. The **canonical authentication root** (`authentication.configDirectory`) is the +root-operated source of truth. It and its regular files are `root:root 0700/0600`. The **runtime +projection** is a separate Linux-only tree for the container reader: its root, `generations`, and +generation directories are `10001:10001 0700`; `CURRENT`, `manifest.json`, `auth.yaml`, and (for +local mode) `users.yaml` are `10001:10001 0600`. The publisher assigns the numeric IDs directly; +it does not create a host user or group for 10001. + +The runtime projection has only `CURRENT` and `generations/<64-lowercase-hex>/`. `CURRENT` selects +one complete immutable generation. A successful configure, user mutation, restore, or explicit +publish first blocks `CURRENT`, then verifies a new immutable generation, then makes it ready. +The selected generation and up to two predecessor generations are retained; no operator edits a +generation or `CURRENT` directly. A ready projection is usable only when its canonical revision is +equal to the current canonical authentication root. If the runtime projection is blocked, missing, +tampered, or unequal, `start`, `update --check-only`, `auth check`, and `doctor` fail closed before +admission or Compose lifecycle work. + +The runtime directory must be an absolute canonical path, distinct from the canonical root, and +must exactly equal `THT_AUTH_RUNTIME_ROOT` in the protected installation environment. The server +profile and numeric UID/GID values are validated before any projected mutation or publication. + +The descriptor loader adds `compose.auth-runtime-projection.yaml` automatically when +`runtimeProjection` is present; do not list that file under `overrides`. The automatic override +mounts the runtime projection **read-only and core-only** at `/run/thothii-auth`; the canonical +authentication root is never mounted. No other service receives that mount or +`THT_AUTH_RUNTIME_PROJECTION_ROOT`. The example descriptor uses +`/srv/example/thothii/auth-runtime` only as a replaceable path and contains no credential value. + +This source change is prepared and tested only: Project A has not been started. It does not +authorize a raw Compose lifecycle launch, an Nginx change, legacy-stack change, or mutation of +`/srv`. A later manual gate needs separate explicit authorization before applying any descriptor +or runtime root to a server. + +### Status, repair, and safe evidence + +Use the root-operated installation command; retain only its small redacted JSON result: + +```sh +sudo tht --installation "$INSTALLATION" auth status --json +``` + +`state: "ready"` and `equal: true` are required before a projected server can start. `state: +"blocked"`, `equal: false`, or a command refusal means that the runtime projection is blocked or +cannot be validated. Do not start the stack, inspect YAML, print an environment, or edit `CURRENT` +or a generation. Confirm the protected canonical root is available, then republish it with: + +```sh +sudo tht --installation "$INSTALLATION" auth publish +sudo tht --installation "$INSTALLATION" auth status --json +``` + +`auth publish` reconstructs the selected immutable generation from the canonical root; it never +uses an older runtime generation as authority. If publish fails, leave the projection blocked and +escalate using the sanitized command result plus descriptor path and timestamp only. Do not attach +passwords, hashes, YAML, raw environment output, `nginx -T`, or a secret-bearing diff to evidence. + +### Authentication restore + +An authentication-bearing restore first publishes a blocked selector, restores canonical +authentication, and publishes a verified candidate generation before any restart. If candidate or +recovery verification fails, the verified recovery checkpoint is republished when possible; an +unverified result remains blocked and prevents start. A restore without authentication entries +does not touch the runtime projection. This is in addition to the normal restore requirement that +browser sessions and pending OIDC state are cleared. + ## Firewall and network boundaries Set `THOTH_SERVER_BIND=127.0.0.1`. Permit inbound TCP 80/443 only to the TLS proxy; port 80 should diff --git a/docs/plans/2026-08-20-psd-server-project-a-standalone.md b/docs/plans/2026-08-20-psd-server-project-a-standalone.md index 6f2d1397..fd2e0882 100644 --- a/docs/plans/2026-08-20-psd-server-project-a-standalone.md +++ b/docs/plans/2026-08-20-psd-server-project-a-standalone.md @@ -251,6 +251,14 @@ Use `profile: server`, the exact new source root/env/auth root, workspace remote access, Project A override, and exactly one Git transport override. Do not include the public session-server overlay in Project A. +For the projected local-auth descriptor, keep `authentication.configDirectory` as the canonical +root and add `runtimeProjection` with a distinct absolute runtime directory plus numeric `uid: 10001` +and `gid: 10001`. The descriptor loader includes the automatic runtime-projection override; do +not list it manually under `overrides`. The canonical root stays `root:root 0700/0600`; the +publisher owns the projection numerically as `10001:10001 0700/0600`. Only the projection is +mounted read-only into core. Do not create a host user/group, edit `CURRENT` or `generations`, or +apply these paths before the separately authorized start gate. + **Step 4: Validate permissions and render** ```bash @@ -274,6 +282,11 @@ Create a temporary mode-0600 password file using an echo-free prompt, then run: Remove the temporary input file after success and record that removal. Do not delete generated `auth.yaml` or `users.yaml`. +Before the later start gate, run the redacted projected status command and require `ready` plus +`equal: true`. A blocked result prevents start. `auth publish` is the only repair path: it rebuilds +from canonical authentication, including after a candidate or recovery restore outcome; it never +promotes a retained runtime generation on its own. + ### Task 6: Build and start the clean stack **Files:** diff --git a/docs/testing/authentication-manual-acceptance.md b/docs/testing/authentication-manual-acceptance.md index c1a6ec51..116b3ace 100644 --- a/docs/testing/authentication-manual-acceptance.md +++ b/docs/testing/authentication-manual-acceptance.md @@ -35,6 +35,13 @@ than inferring a PASS. 4. Confirm the exact direct `groups` claim for both identities and the mappings `TOT Users → user` and `TOT Admin → admin`. Confirm extra upstream groups are ignored without warning. +5. For a projected Linux server, before any start gate, collect only the redacted result of + `sudo tht --installation "$INSTALLATION" auth status --json`. Record `state`, generation, + canonical revision, and `equal`; do not retain authentication YAML, user records, hashes, or + environment output. `ready` plus `equal: true` is required. A blocked or unequal result is a + fail-closed condition: do not start, and use `sudo tht --installation "$INSTALLATION" auth + publish` followed by the same status command only after the canonical root is available. + ## Matrix | Scenario | Expected result | @@ -56,6 +63,7 @@ than inferring a PASS. | Logout | Cookie expires and the server session is deleted. | | Provider outage | Live check reports `oidc_discovery_unreachable`; browser login fails closed without exposing credentials. | | Restore is completed | Sessions and OIDC state are absent; all users must reauthenticate. | +| Projected authentication restore | Candidate generation and any recovery generation are published from the canonical root; a failed verification remains blocked and start is refused. | ## Status at Task 15 diff --git a/docs/testing/psd-server-project-a-manual.md b/docs/testing/psd-server-project-a-manual.md index 47f7de02..786a1cd9 100644 --- a/docs/testing/psd-server-project-a-manual.md +++ b/docs/testing/psd-server-project-a-manual.md @@ -29,6 +29,13 @@ verdi installazione, workspace, DWH, Qdrant, Ollama e preprocessing. ## 1. Stato generale +Prima del gate manuale di avvio, per una descriptor server con runtime projection eseguire solo il +controllo redatto `sudo tht --installation "$INSTALLATION" auth status --json`. Il risultato deve +dire `ready` ed `equal: true`. Se è `blocked`, mancante o diverso dal canonical root, non avviare: +Sol può eseguire `sudo tht --installation "$INSTALLATION" auth publish` e ripetere il controllo, +senza copiare YAML, hash, password, token o environment nel rapporto. Questo documento non +autorizza l'avvio; Project A resta soggetto a un'esplicita autorizzazione separata. + Eseguire: ```bash diff --git a/scripts/auth-docs-smoke.sh b/scripts/auth-docs-smoke.sh index 1801a164..1c9f16f3 100755 --- a/scripts/auth-docs-smoke.sh +++ b/scripts/auth-docs-smoke.sh @@ -157,4 +157,51 @@ if rg -n -i --pcre2 '(?:unmapped|additional|extra)[^.\r\n]{0,160}groups?[^.\r\n] exit 1 fi +# Projected server authentication documentation contract. +projection_docs=( + "$root/docs/install/server.md" + "$root/docs/install/authentication-local.md" + "$root/docs/testing/authentication-manual-acceptance.md" + "$root/docs/testing/psd-server-project-a-manual.md" + "$root/docs/plans/2026-08-20-psd-server-project-a-standalone.md" + "$root/PROJECT_STATE.md" +) +projection_corpus=$(mktemp) +trap 'rm -f "$corpus" "$projection_corpus"' EXIT +cat "${projection_docs[@]}" >"$projection_corpus" +projection_required=( + "canonical authentication root" "runtime projection" "CURRENT" "generations" + "root:root 0700/0600" "10001:10001 0700/0600" + "THT_AUTH_RUNTIME_ROOT" + "auth status --json" "auth publish" + "candidate or recovery" "Mac, Windows, and local direct-file authentication" + "explicit authorization" +) +for term in "${projection_required[@]}"; do + rg -Fqi "$term" "$projection_corpus" || { + echo "auth docs smoke: missing runtime-projection term: $term" >&2 + exit 1 + } +done +if rg -n -i --pcre2 "(?:useradd|groupadd)[^\n]{0,100}10001|10001[^\n]{0,100}(?:useradd|groupadd)" "$projection_corpus"; then + echo "auth docs smoke: runtime projection creates a host 10001 identity" >&2 + exit 1 +fi +if rg -n -i --pcre2 '(?:^|[.!?]\s+)(?:(?:mount|bind)\s+(?:the\s+)?(?:canonical authentication root|auth-canonical)\b[^\n]{0,120}(?:core|/run/thothii-auth)|(?:core|the core service)\s+(?:mounts?|binds?)\s+(?:the\s+)?(?:canonical authentication root|auth-canonical)\b)|auth-canonical[^\n]{0,120}:/run/thothii-auth' "$projection_corpus"; then + echo "auth docs smoke: canonical authentication is mounted into core" >&2 + exit 1 +fi +if rg -n -i --pcre2 '(?:(?:operators?\s+)?(?:may|can|should|must)\s+(?!not\b|never\b)(?:edit|write|modify|prune)|^\s*(?:sudo\s+)?(?:vi|vim|nano|sed|cp|mv|rm|tee|printf|echo)\b)[^\n]*(?:CURRENT|generations)' "$projection_corpus"; then + echo "auth docs smoke: runtime projection permits direct selector or generation edits" >&2 + exit 1 +fi +if rg -n -i --pcre2 '(?:^\s*(?:sudo\s+nginx\s+-T|(?:sudo\s+)?(?:printenv|env)(?:\s|$)|(?:sudo\s+)?(?:diff|cat|less|more|head|tail|yq)\s+[^\n]*(?:secret|auth\.yaml|users\.yaml))|(?:may|can|should|must)\s+(?!not\b|never\b)(?:dump|print|capture|attach|include|run)\b[^\n]*(?:password|auth\.yaml|users\.yaml|YAML|raw environment|nginx\s+-T|secret-bearing diff))' "$projection_corpus"; then + echo "auth docs smoke: runtime projection documents unsafe evidence collection" >&2 + exit 1 +fi +if rg -n -i --pcre2 'Project A (?:has been|was|is) started|legacy[- ]stack (?:has been|was|is) changed' "$projection_corpus"; then + echo "auth docs smoke: runtime projection claims live Project A or legacy mutation" >&2 + exit 1 +fi + echo "auth docs smoke: required terms and forbidden wording checks passed" diff --git a/scripts/test-project-a-auth-runtime-projection.sh b/scripts/test-project-a-auth-runtime-projection.sh new file mode 100755 index 00000000..8603ec75 --- /dev/null +++ b/scripts/test-project-a-auth-runtime-projection.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# Hermetic acceptance gate for the server authentication runtime projection. +set -euo pipefail +umask 077 + +root="$(cd "$(dirname "$0")/.." && pwd -P)" +forbidden_server_root="/$(printf '%s' srv)/" +tmp_base="$(cd "${TMPDIR:-/tmp}" && pwd -P)" +if [[ "$tmp_base/" == "$forbidden_server_root"* ]]; then + echo "runtime projection gate refuses a server temp root" >&2 + exit 1 +fi +if [[ -n "${DOCKER_HOST:-}" ]]; then + echo "runtime projection gate refuses a remote Docker endpoint" >&2 + exit 1 +fi +docker_context="$(docker context show)" +docker_endpoint="$(docker context inspect "$docker_context" --format '{{(index .Endpoints "docker").Host}}')" +if [[ "$docker_endpoint" != unix:///* ]]; then + echo "runtime projection gate requires a local Unix Docker endpoint" >&2 + exit 1 +fi +tmp="$(mktemp -d "$tmp_base/thoth-auth-runtime-projection.XXXXXX")" +chmod 0700 "$tmp" +mkdir -m 0700 "$tmp/go-mod" "$tmp/go-build" + +cleanup() { + local cleanup_status=0 + if [[ -d "$tmp" ]]; then + docker run --rm --network none -v "$tmp:/cleanup" golang:1.26.5 \ + sh -c 'find /cleanup -mindepth 1 -delete' >/dev/null 2>&1 || cleanup_status=$? + if ((cleanup_status == 0)); then + rmdir "$tmp" || cleanup_status=$? + fi + fi + return "$cleanup_status" +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +project_name_flag="--project"-name +compose_project_var='COMPOSE_PROJECT'_NAME +network_flag="--net"work +compose_pattern='docker[[:space:]]+com''pose' +if rg -n -F -- "$forbidden_server_root" "$0"; then + echo "runtime projection gate must not reference a server path" >&2 + exit 1 +fi +if rg -n -- "$project_name_flag|$compose_project_var|${compose_pattern}[^[:cntrl:]]*(up|start)|docker[[:space:]]+network" "$0"; then + echo "runtime projection gate has a forbidden project, lifecycle, or network attachment" >&2 + exit 1 +fi +if rg -n -F -- "$network_flag" "$0" | rg -v -F -e "$network_flag none" -e "$network_flag=none"; then + echo "runtime projection gate permits only an explicitly isolated network option" >&2 + exit 1 +fi +if rg -n --pcre2 '^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*(?:PASSWORD|SECRET|TOKEN|KEY)[A-Za-z_]*=' "$0"; then + echo "runtime projection gate declares a credential value" >&2 + exit 1 +fi + +node24_source="$( + env -i PATH="$PATH" HOME="$HOME" npm_config_offline=true npm_config_userconfig=/dev/null \ + npx -y -p node@24 node -p 'process.execPath' +)" +if [[ "$node24_source" != /* || ! -f "$node24_source" || ! -x "$node24_source" ]]; then + echo "runtime projection gate requires a cached Node 24 runtime" >&2 + exit 1 +fi +install -m 0755 "$node24_source" "$tmp/node24" + +run_case() { + local name="$1" + shift + local output="$tmp/$name.log" + if ! "$@" >"$output" 2>&1; then + echo "$name FAIL (test output suppressed and removed by cleanup)" >&2 + return 1 + fi + printf '%s PASS\n' "$name" +} + +go_test() { + docker run --rm \ + -v "$root:/work:ro" \ + -v "$tmp/go-mod:/go/pkg/mod" \ + -v "$tmp/go-build:/root/.cache/go-build" \ + -w /work/tools/tht golang:1.26.5 go test "$@" +} + +go_windows_compile() { + docker run --rm \ + -v "$root:/work:ro" \ + -v "$tmp/go-mod:/go/pkg/mod" \ + -v "$tmp/go-build:/root/.cache/go-build" \ + -w /work/tools/tht -e GOOS=windows -e GOARCH=amd64 golang:1.26.5 \ + go test -c ./cmd/tht -o /tmp/tht.test.exe +} + +go_darwin_compile() { + docker run --rm \ + -v "$root:/work:ro" \ + -v "$tmp/go-mod:/go/pkg/mod" \ + -v "$tmp/go-build:/root/.cache/go-build" \ + -w /work/tools/tht -e GOOS=darwin -e GOARCH=amd64 golang:1.26.5 \ + go test -c ./cmd/tht -o /tmp/tht.test +} + +backend_node24() { + docker run --rm --network none \ + -e HOME=/tmp \ + -v "$root:/work:ro" \ + -v "$tmp/node24:/opt/node24:ro" \ + -v "$root/backend/vitest.config.ts:/opt/vitest.config.ts:ro" \ + -v "$root/backend/node_modules:/opt/node_modules:ro" \ + -w /work/backend golang:1.26.5 \ + /opt/node24 node_modules/vitest/vitest.mjs run --no-cache \ + --config /opt/vitest.config.ts "$@" +} + +backend_in_flight() { + backend_node24 test/auth-runtime-projection.test.ts \ + -t 'in-flight snapshot authenticates A after selection B and deletion A' +} + +backend_direct_file() { + backend_node24 test/config.test.ts \ + -t 'keeps the direct auth-file provider when the runtime projection environment is absent' +} + +mac_windows_direct_file() { + go_windows_compile + go_darwin_compile + backend_direct_file +} + +run_case descriptor_requires_server_uid_gid_and_matching_env \ + go_test ./internal/config -run 'TestLoadAcceptsServerRuntimeProjectionAndPlacesAutomaticOverrideBeforeCurrentImage|TestLoadRejectsInvalidRuntimeProjection|TestLoadRejectsRuntimeProjectionEnvironmentWithoutDescriptor' -count=1 +run_case compose_mount_is_core_only_read_only_and_noncanonical \ + bash "$root/scripts/test-auth-runtime-projection-compose.sh" +run_case local_initial_configure_publishes_equal_ready \ + go_test ./internal/setup ./internal/authconfig -run 'TestRunConfigureOnlyPublishesInitialProjectedServerAuthentication|TestRunProjectedMutationPublishesExactLocalAndOIDCSnapshots/local' -count=1 +run_case oidc_initial_configure_publishes_equal_ready \ + go_test ./internal/authconfig -run 'TestRunProjectedMutationPublishesExactLocalAndOIDCSnapshots/oidc' -count=1 +run_case every_user_mutation_blocks_then_publishes \ + go_test ./internal/authconfig -run 'TestProjectedAuthMutatorsBlockBeforeCanonicalWriteAndPublishOnlyEqualSnapshots' -count=1 +run_case publish_repairs_blocked_from_canonical \ + go_test ./internal/authconfig -run 'TestPublishProjectedCanonicalRepairsBlockedStateFromCanonicalOnly|TestProjectedAuthPublishStatusAndCheckFailClosed' -count=1 +run_case start_and_doctor_fail_closed_for_missing_blocked_tampered_or_divergent \ + go_test ./internal/authprojection ./internal/authconfig ./internal/service ./internal/doctor ./cmd/tht -run 'TestInspectRejectsMissingBlockedMalformedAndTamperedCurrent|TestRequireRuntimeAuthProjectionReadyRejectsMissingBlockedAndDivergentStates|TestStartRefusesProjectedAuthenticationBeforeComposeWhenNotReady|TestRunReportsOneSanitizedRuntimeAuthProjectionFailureBeforeCompose|TestRunUpdateCheckOnlyRefusesProjectedAuthenticationBeforeCompose' -count=1 +run_case backend_authenticates_from_one_immutable_generation_after_previous_gc backend_in_flight +run_case auth_restore_publishes_candidate \ + go_test ./internal/backup -run 'TestRestoreAuthBearingArchiveBlocksBeforeWritePublishesBeforeRestart' -count=1 +run_case failed_candidate_verification_republishes_checkpoint \ + go_test ./internal/backup -run 'TestRestoreAuthVerificationFailuresRepublishCheckpointBeforeRecoveryRestart' -count=1 +run_case failed_recovery_publish_remains_blocked \ + go_test ./internal/backup -run 'TestRestoreAuthCandidatePublicationFailureRecoversThenStaysBlockedWithoutRestart' -count=1 +run_case non_auth_restore_never_touches_projection \ + go_test ./internal/backup -run 'TestRestoreNonAuthArchiveNeverBeginsProjection' -count=1 +run_case mac_windows_local_regression mac_windows_direct_file +run_case secret_redaction \ + go_test ./internal/authconfig -run 'TestProjectionCoordinatorErrorsAndLogsNeverContainSyntheticPasswordsOrHashes' -count=1 + +echo "runtime authentication projection acceptance gate passed." diff --git a/scripts/test-verify-workspace-install-docs.sh b/scripts/test-verify-workspace-install-docs.sh index 1a49ae26..599450bb 100755 --- a/scripts/test-verify-workspace-install-docs.sh +++ b/scripts/test-verify-workspace-install-docs.sh @@ -1106,4 +1106,89 @@ if (( negative_failures != 0 )); then exit 1 fi +# Projected server authentication documents must preserve the root-only canonical/runtime split. +projection_example="$root/docs/install/examples/thothii-installation.server.yaml" +for required in \ + 'runtimeProjection:' \ + 'directory: "/srv/example/thothii/auth-runtime"' \ + 'uid: 10001' \ + 'gid: 10001'; do + grep -Fq -- "$required" "$projection_example" || { + echo "server authentication projection example lacks: $required" >&2 + exit 1 + } +done +if rg -n -i --pcre2 '(?:password|secret)[[:space:]]*:[[:space:]]*[^<#[:space:]]+' "$projection_example"; then + echo "server authentication projection example contains a credential value" >&2 + exit 1 +fi + +projection_docs=( + "$root/docs/install/server.md" + "$root/docs/install/authentication-local.md" + "$root/docs/testing/authentication-manual-acceptance.md" + "$root/docs/testing/psd-server-project-a-manual.md" + "$root/docs/plans/2026-08-20-psd-server-project-a-standalone.md" + "$root/PROJECT_STATE.md" +) +projection_corpus="$negative_root/projection-corpus.md" +cat "${projection_docs[@]}" >"$projection_corpus" + +projection_documentation_is_safe() { + local corpus="$1" + if rg -q -i --pcre2 '(?:useradd|groupadd)[^\n]{0,100}10001|10001[^\n]{0,100}(?:useradd|groupadd)' "$corpus"; then + return 1 + fi + if rg -q -i --pcre2 '(?:^|[.!?]\s+)(?:(?:mount|bind)\s+(?:the\s+)?(?:canonical authentication root|auth-canonical)\b[^\n]{0,120}(?:core|/run/thothii-auth)|(?:core|the core service)\s+(?:mounts?|binds?)\s+(?:the\s+)?(?:canonical authentication root|auth-canonical)\b)|auth-canonical[^\n]{0,120}:/run/thothii-auth' "$corpus"; then + return 1 + fi + if rg -q -i --pcre2 '(?:(?:operators?\s+)?(?:may|can|should|must)\s+(?!not\b|never\b)(?:edit|write|modify|prune)|^\s*(?:sudo\s+)?(?:vi|vim|nano|sed|cp|mv|rm|tee|printf|echo)\b)[^\n]*(?:CURRENT|generations)' "$corpus"; then + return 1 + fi + if rg -q -i --pcre2 '(?:^\s*(?:sudo\s+nginx\s+-T|(?:sudo\s+)?(?:printenv|env)(?:\s|$)|(?:sudo\s+)?(?:diff|cat|less|more|head|tail|yq)\s+[^\n]*(?:secret|auth\.yaml|users\.yaml))|(?:may|can|should|must)\s+(?!not\b|never\b)(?:dump|print|capture|attach|include|run)\b[^\n]*(?:password|auth\.yaml|users\.yaml|YAML|raw environment|nginx\s+-T|secret-bearing diff))' "$corpus"; then + return 1 + fi + if rg -q -i --pcre2 'Project A (?:has been|was|is) started|legacy[- ]stack (?:has been|was|is) changed' "$corpus"; then + return 1 + fi + return 0 +} + +for required in \ + 'canonical authentication root' \ + 'read-only and core-only' \ + 'candidate or recovery' \ + 'runtime projection is blocked' \ + 'Project A has not been started'; do + if ! rg -Fqi -- "$required" "${projection_docs[@]}"; then + echo "server authentication projection documentation lacks: $required" >&2 + exit 1 + fi +done +if ! projection_documentation_is_safe "$projection_corpus"; then + echo "server authentication projection documentation contains an unsafe instruction or claim" >&2 + exit 1 +fi + +while IFS='|' read -r fixture_name payload; do + fixture="$negative_root/projection-$fixture_name.md" + cp "$projection_corpus" "$fixture" + printf '\n%s\n' "$payload" >>"$fixture" + if projection_documentation_is_safe "$fixture"; then + echo "server authentication projection documentation accepted unsafe fixture: $fixture_name" >&2 + exit 1 + fi +done <<'PROJECTION_NEGATIVE_FIXTURES' +host-identity|sudo useradd --system --uid 10001 thothii +canonical-core-mount|Mount the canonical authentication root into core at /run/thothii-auth. +core-subject-canonical-mount|Core mounts the canonical authentication root at /run/thothii-auth. +direct-runtime-edit|Operators may edit CURRENT and prune generations directly. +yaml-dump|Operators may dump auth.yaml and users.yaml into evidence. +nginx-dump|sudo nginx -T +raw-environment|printenv +sudo-raw-environment|sudo printenv +secret-diff|sudo diff auth.yaml auth.yaml.previous +live-claim|Project A has been started. +PROJECTION_NEGATIVE_FIXTURES + echo "unsafe installation-document fixtures rejected passed"