# DWH REST Per-Installation Authentication Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build, document, and safely activate a reusable server-side `dwh-auth` service that gives every remote ThothII installation an independently revocable `/dwh/` API key without changing portable ThothII connector behavior. **Architecture:** A standalone Linux Go binary uses only the standard library, stores versioned credential digests in protected atomic files, and serves one fail-closed verification endpoint over a Unix socket. Host Nginx uses that endpoint through `auth_request`; PSD runs the binary as an independent hardened `systemd` service, while the local ThothII server continues to use `postgres_direct`. **Tech Stack:** Go 1.26.0/toolchain 1.26.5, standard library only, Linux `systemd`, Nginx `auth_request`, Bash verification scripts, Markdown documentation, Git. ## Global Constraints - Preserve the portable REST contract: successful clients continue to send exactly one `X-API-Key` header. - Do not modify `postgres_direct` or `ssh_tunnel`, and do not add DWH commands to portable `tht`. - Do not add `dwh-auth` to ThothII Compose or `tht start/stop`. - Build Linux amd64/arm64 only; Mac and Windows do not need this binary. - Use only the Go standard library; do not add SQLite, CGO, or runtime packages. - New keys use `thtdwh_v1.<16-character-base64url-key-id>.<43-character-base64url-secret>` with 256 random secret bits. - Permit one temporary unversioned `legacy_raw` record with public ID `legacy-shared`; revoke it after Mac migration. - Store SHA-256 digests and non-secret metadata only; never emit digests through CLI or HTTP. - Default to no expiry; optional expiry is RFC3339 UTC and fail-closed. - Read/write real keys only through absolute protected files. Never put keys in argv, env, Git, logs, JSON output, or ordinary config. - Credential failures are generic `401`; service/registry/integrity failures become `503`. - PSD uses independent `systemd` plus a Unix socket. Stopping ThothII must not stop DWH auth. - Do not preserve/migrate legacy test sessions, Qdrant indexes, or Ollama caches. - Do not stop or modify the legacy ThothII stack in this plan. - Do not mutate PSD Nginx, `systemd`, registry paths, or real keys before Task 9 authorization. - When delegated, use Luna for deterministic execution and Terra for review. The primary executor owns secret-bearing production steps and reports sanitized results. - Keep unrelated `brain/` changes and other dirty content out of every commit. --- ### Task 1: Freeze credential and record contracts **Files:** - Create: `tools/dwh-auth/go.mod` - Create: `tools/dwh-auth/internal/credential/credential.go` - Create: `tools/dwh-auth/internal/credential/credential_test.go` - Create: `tools/dwh-auth/internal/record/record.go` - Create: `tools/dwh-auth/internal/record/record_test.go` **Interfaces:** - Consumes: Go standard-library randomness, hashing, constant-time comparison, base64url, JSON, and time. - Produces: `credential.Generate(io.Reader) (Material, error)`, `credential.VerifyV1([]byte, record.Digest) (string, bool)`, `credential.VerifyLegacy([]byte, record.Digest) bool`, `record.Validate(Record) error`, and the record JSON schema. - [ ] **Step 1: Add the isolated module** ```go module github.com/aritmolab/thothii/tools/dwh-auth go 1.26.0 toolchain go1.26.5 ``` - [ ] **Step 2: Write failing format and validation tests** Freeze: ```go const Prefix = "thtdwh_v1" const KeyIDEncodedLength = 16 const SecretEncodedLength = 43 const MaxHeaderBytes = 128 type Material struct { Value []byte KeyID string Digest record.Digest } ``` Tests use a deterministic 44-byte reader and assert exact segments/lengths, 32 decoded secret bytes, valid digest, changed-byte rejection, comma/duplicate rejection, padding/whitespace rejection, and 129-byte rejection. Legacy tests hash opaque raw values without v1 parsing. Record tests cover every invalid enum, identifier, timestamp, digest, description, expiry, and revocation combination. - [ ] **Step 3: Prove RED** ```bash cd tools/dwh-auth go test ./internal/credential ./internal/record -count=1 ``` Expected: non-zero because implementations are absent. - [ ] **Step 4: Implement the exact record schema** ```go type Kind string const ( SchemaVersion = 1 KindV1 Kind = "per_installation_v1" KindLegacyRaw Kind = "legacy_raw" LegacyKeyID = "legacy-shared" ) type Digest [32]byte type Record struct { SchemaVersion int `json:"schema_version"` Kind Kind `json:"credential_kind"` KeyID string `json:"key_id"` InstallationID string `json:"installation_id"` Description string `json:"description,omitempty"` SecretSHA256 string `json:"secret_sha256"` CreatedAt time.Time `json:"created_at"` ExpiresAt *time.Time `json:"expires_at,omitempty"` RevokedAt *time.Time `json:"revoked_at,omitempty"` RevocationReason string `json:"revocation_reason,omitempty"` } ``` Require schema 1; exact kind/key relationship; installation ID `[a-z0-9][a-z0-9-]{0,62}`; metadata at most 160 UTF-8 characters without controls; canonical 32-byte base64url digest; UTC timestamps; expiry after creation; paired revocation fields. - [ ] **Step 5: Implement generation and verification** Read 12 random key-ID bytes and 32 secret bytes, use `base64.RawURLEncoding`, emit the 70-byte canonical value, hash the complete value, and compare via `subtle.ConstantTimeCompare`. Legacy accepts 1–128 opaque bytes without ASCII controls and hashes the entire raw value. - [ ] **Step 6: Prove GREEN and standard-library-only** ```bash cd tools/dwh-auth gofmt -w internal/credential internal/record go test ./internal/credential ./internal/record -count=1 test "$(go list -m all)" = 'github.com/aritmolab/thothii/tools/dwh-auth' ``` Expected: tests pass; the exact module list contains only the main module, proving there are no external module dependencies (the Go standard library is not listed as a module). - [ ] **Step 7: Commit** ```bash git add tools/dwh-auth/go.mod tools/dwh-auth/internal/credential tools/dwh-auth/internal/record git commit -m "feat: define DWH installation credentials" ``` --- ### Task 2: Implement the protected atomic registry **Files:** - Create: `tools/dwh-auth/internal/securefile/securefile_linux.go` - Create: `tools/dwh-auth/internal/securefile/securefile_linux_test.go` - Create: `tools/dwh-auth/internal/registry/store.go` - Create: `tools/dwh-auth/internal/registry/store_test.go` **Interfaces:** - Consumes: Task 1 records and key IDs. - Produces: `registry.Open`, `Store.Add`, `Store.Find`, `Store.FindLegacy`, `Store.List`, `Store.Revoke`, and `Store.Check`. - [ ] **Step 1: Write failing security tests** Cover regular protected files; symlinked roots/directories/records/secret files; unsafe modes; unknown/duplicate/trailing JSON; filename mismatch; partial files; concurrent reads; and revoked state winning when both records exist. Use `t.TempDir` and Linux build tags. - [ ] **Step 2: Prove RED** ```bash cd tools/dwh-auth go test ./internal/securefile ./internal/registry -count=1 ``` - [ ] **Step 3: Implement no-follow bounded reads/writes** Use `syscall.Open` with `O_NOFOLLOW|O_CLOEXEC`, compare `Fstat`/`Lstat`, require regular files, reject group/world write, cap record reads at 4096 bytes, and detect size changes. Secret input is `0600`; generated output uses `O_CREAT|O_EXCL` and `0600`. - [ ] **Step 4: Implement state contracts** ```go type State string const ( StateActive State = "active" StateRevoked State = "revoked" ) type PublicRecord struct { Kind record.Kind `json:"credential_kind"` KeyID string `json:"key_id"` InstallationID string `json:"installation_id"` Description string `json:"description,omitempty"` CreatedAt time.Time `json:"created_at"` ExpiresAt *time.Time `json:"expires_at,omitempty"` State State `json:"state"` RevokedAt *time.Time `json:"revoked_at,omitempty"` RevocationReason string `json:"revocation_reason,omitempty"` } ``` `Add` uses exclusive temp, canonical JSON plus newline, record mode `0640`, sync, rename, and directory sync. `Revoke` publishes revoked state before removing active state. `Find` checks revoked first. `FindLegacy` allows at most one `legacy_raw`; multiple records are an integrity fault. - [ ] **Step 5: Prove GREEN** ```bash cd tools/dwh-auth gofmt -w internal/securefile internal/registry go test -race ./internal/securefile ./internal/registry -count=1 ``` - [ ] **Step 6: Commit** ```bash git add tools/dwh-auth/internal/securefile tools/dwh-auth/internal/registry git commit -m "feat: add protected DWH credential registry" ``` --- ### Task 3: Add the secret-safe administrative CLI **Files:** - Create: `tools/dwh-auth/internal/command/command.go` - Create: `tools/dwh-auth/internal/command/command_test.go` - Create: `tools/dwh-auth/cmd/dwh-auth/main.go` **Interfaces:** - Consumes: Tasks 1–2. - Produces: `command.Run(context.Context, []string, io.Reader, io.Writer, io.Writer) int`. - [ ] **Step 1: Write failing CLI tests** Freeze exits 0 success, 2 unsafe invocation, 3 not found, 4 integrity/filesystem. Prove create writes once to new `0600`; import reads `0600`; no overwrite; list/status omit digest; revoke requires reason; relative/secret-valued flags fail; sentinel secret never enters output. - [ ] **Step 2: Prove RED** ```bash cd tools/dwh-auth go test ./internal/command -count=1 ``` - [ ] **Step 3: Implement only this grammar** ```text dwh-auth --registry-root ABS key create --installation-id ID [--description TEXT] [--expires-at RFC3339] --output ABS dwh-auth --registry-root ABS key import --legacy-raw --installation-id legacy-shared --from-file ABS dwh-auth --registry-root ABS key list [--json] dwh-auth --registry-root ABS key status --key-id ID [--json] dwh-auth --registry-root ABS key revoke --key-id ID --reason TEXT dwh-auth --registry-root ABS check [--json] dwh-auth serve --registry-root ABS --socket ABS ``` No aliases, env fallback, interactive secret, or plaintext output. Create prints only `created key_id= installation_id= output=`. - [ ] **Step 4: Implement failure rollback** Create writes/syncs protected output before record publication and removes output if publication fails. Import never modifies its source. Cleanup uncertainty returns exit 4 and path only. - [ ] **Step 5: Prove GREEN** ```bash cd tools/dwh-auth gofmt -w cmd internal/command go test -race ./internal/command -count=1 go test ./... -count=1 ``` - [ ] **Step 6: Commit** ```bash git add tools/dwh-auth/cmd tools/dwh-auth/internal/command git commit -m "feat: add DWH credential administration CLI" ``` --- ### Task 4: Implement Unix-socket verification **Files:** - Create: `tools/dwh-auth/internal/service/service.go` - Create: `tools/dwh-auth/internal/service/service_test.go` - Modify: `tools/dwh-auth/internal/command/command.go` - Modify: `tools/dwh-auth/internal/command/command_test.go` **Interfaces:** - Consumes: Tasks 1–3. - Produces: `service.New(*registry.Store, *log.Logger, func() time.Time) http.Handler`, `service.ListenAndServe(context.Context, Config) error`, and `GET /verify`. - [ ] **Step 1: Write failing decision tests** Valid v1=204/key ID; legacy=204/legacy ID; absent/duplicate/malformed/unknown/changed/expired/ revoked=401; corrupt/unsafe/multiple-legacy=503; other path=404; other method=405. Bodies empty. Logs contain timestamp, decision code, parsed public ID only—never secret/digest/description/query. - [ ] **Step 2: Prove RED** ```bash cd tools/dwh-auth go test ./internal/service -count=1 ``` - [ ] **Step 3: Implement handler/listener** Require exactly one header value and 128-byte bound. Try strict v1 then reserved legacy for non-v1. Require canonical absolute socket/registry paths, refuse non-socket collision, remove only a proven owned stale socket, chmod `0660`, and stop on context cancellation. - [ ] **Step 4: Wire `serve` fail-closed** Dispatch `serve` before administrative write access. Open registry read-only, run `Check`, and refuse startup on malformed or unsafe state. A well-formed expired record remains valid registry data but always authenticates as denied. - [ ] **Step 5: Verify** ```bash cd tools/dwh-auth gofmt -w cmd internal/command internal/service go test -race ./... -count=1 go vet ./... ``` - [ ] **Step 6: Commit** ```bash git add tools/dwh-auth/cmd tools/dwh-auth/internal/command tools/dwh-auth/internal/service git commit -m "feat: serve DWH authentication over Unix socket" ``` --- ### Task 5: Add reproducible packaging and hardened templates **Files:** - Create: `docker/dwh-auth.Dockerfile` - Create: `scripts/build-dwh-auth.sh` - Create: `scripts/test-dwh-auth-build-contract.sh` - Create: `deploy/dwh-auth/dwh-auth.service` - Create: `deploy/dwh-auth/dwh-auth.tmpfiles.conf` - Create: `deploy/dwh-auth/nginx-http.conf.example` - Create: `deploy/dwh-auth/nginx-dwh-location.conf.example` **Interfaces:** - Consumes: Task 4 binary. - Produces: static Linux amd64/arm64 outputs and generic deployment templates. - [ ] **Step 1: Write failing contract** Require the exact pinned builder digest from `docker/tht.Dockerfile`, no `require` in go.mod, non-empty ELF outputs, and service directives `User=dwh-auth`, `Group=www-data`, `SupplementaryGroups=dwh-auth`, `NoNewPrivileges=true`, `ProtectSystem=strict`, `ProtectHome=true`, `RestrictAddressFamilies=AF_UNIX`, empty `CapabilityBoundingSet`, `UMask=0007`. Tmpfiles creates root/active/revoked as `root:dwh-auth` mode `2750`. - [ ] **Step 2: Prove RED** ```bash bash scripts/test-dwh-auth-build-contract.sh ``` - [ ] **Step 3: Add static builds** Docker runs tests, then builds only linux/amd64 and linux/arm64 with `CGO_ENABLED=0`, `-trimpath`, `-ldflags='-s -w'`. Build script accepts `--output ABSOLUTE_DIRECTORY`, rejects unsafe paths, and defaults to `dist/dwh-auth`. - [ ] **Step 4: Add systemd/storage templates** ```ini ExecStart=/usr/local/sbin/dwh-auth serve --registry-root /var/lib/dwh-auth --socket /run/dwh-auth/verify.sock ``` Service writes only `RuntimeDirectory=dwh-auth`, reads registry, and has no secret environment. - [ ] **Step 5: Add Nginx templates** HTTP map/zone rate key contains only remote address plus parsed public ID, never secret; rate is 20/s. Location uses Unix auth, `GET /verify`, no body, uses a trailing-slash upstream so public `/dwh/rpc/ping?x` reaches PostgREST as `/rpc/ping?x`, clears `X-API-Key` before PostgREST, burst 100, keeps the public `/dwh/` authorization boundary while stripping that prefix upstream, maps auth infrastructure failure to 503. - [ ] **Step 6: Prove GREEN** ```bash bash -n scripts/build-dwh-auth.sh bash -n scripts/test-dwh-auth-build-contract.sh bash scripts/test-dwh-auth-build-contract.sh ``` Expected final output: `dwh-auth build and deployment contract passed`. - [ ] **Step 7: Commit** ```bash git add docker/dwh-auth.Dockerfile scripts/build-dwh-auth.sh scripts/test-dwh-auth-build-contract.sh deploy/dwh-auth git commit -m "build: package standalone DWH authentication service" ``` --- ### Task 6: Gate Nginx and CI behavior **Files:** - Create: `scripts/test-dwh-auth-nginx-integration.sh` - Create: `scripts/test-dwh-auth-nginx-contract.sh` - Modify: `.github/workflows/deployment.yml` **Interfaces:** - Consumes: Tasks 1–5. - Produces: structural/runtime Nginx gates and `dwh-auth-linux` CI. - [ ] **Step 1: Write failing structural checks** Require effective (non-comment) directives: ```nginx auth_request /_check_dwh_key; proxy_method GET; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-API-Key $http_x_api_key; proxy_set_header X-API-Key ""; error_page 500 =503 @dwh_auth_unavailable; ``` Negative fixtures remove each and test public verifier, TCP authenticator, PostgREST bypass, full secret in rate key, and failure mapped to success. - [ ] **Step 2: Prove RED** ```bash bash scripts/test-dwh-auth-nginx-contract.sh ``` - [ ] **Step 3: Implement runtime smoke** Use one temp root, synthetic v1/legacy keys, temp auth/Nginx Unix sockets, and a static marker behind auth. Assert v1=200/body unchanged, legacy=200, invalid=401, revoked=401, expired=401, stopped authenticator=503. Trap only recorded PIDs and exact temp root. - [ ] **Step 4: Add CI job** Checkout without credentials, Go 1.26.5 keyed by `tools/dwh-auth/go.mod`, install Ubuntu `nginx-light`, then: ```bash (cd tools/dwh-auth && go test -race ./... -count=1 && go vet ./...) bash scripts/test-dwh-auth-build-contract.sh bash scripts/test-dwh-auth-nginx-contract.sh bash scripts/test-dwh-auth-nginx-integration.sh ``` - [ ] **Step 5: Run locally to GREEN** Expected: all exit 0; output includes case/status only. - [ ] **Step 6: Commit** ```bash git add scripts/test-dwh-auth-nginx-integration.sh scripts/test-dwh-auth-nginx-contract.sh .github/workflows/deployment.yml git commit -m "test: gate DWH authentication integration" ``` --- ### Task 7: Write and verify operator documentation **Files:** - Create: `docs/install/dwh-auth-server.md` - Create: `docs/install/dwh-auth-client-enrollment.md` - Create: `docs/install/dwh-auth-tls.md` - Create: `docs/operations/psd-dwh-auth-rollout.md` - Create: `docs/testing/dwh-auth-manual-acceptance.md` - Create: `docs/testing/evidence/psd-dwh-auth-rollout-report-template.md` - Create: `scripts/verify-dwh-auth-docs.sh` - Create: `scripts/test-verify-dwh-auth-docs.sh` - Modify: `docs/install/local-workspace-registry.md` - Modify: `docs/install/server-workspace-registry.md` - Modify: `docs/install/psd-workspace-setup.md` - Modify: `docs/guida-utente.md` - Modify: `docs/index.md` - Modify: `mkdocs.yml` **Interfaces:** - Consumes: Tasks 1–6. - Produces: generic server/client/TLS manuals, PSD runbook, manual acceptance, sanitized evidence, navigation, docs gates. - [ ] **Step 1: Write failing docs fixtures** Require prerequisites/install/issue/deliver/GUI/headless/rotate/revoke/TLS/fingerprint/renewal/ rollback/401/503/evidence/uninstall. Reject key/digest literals, `curl -k`, disabled TLS, secrets in env/argv, world-readable files, raw Nginx capture, and Compose coupling. - [ ] **Step 2: Prove RED** ```bash bash scripts/test-verify-dwh-auth-docs.sh ``` - [ ] **Step 3: Write generic manuals** Document exact paths, identities/modes, safe commands, one key per installation, rotation generations, optional expiry, protected delivery, GUI vault, headless `API_KEY_FILE`, `/rpc/ping`, positive/negative checks, registry backup, revocation, troubleshooting. - [ ] **Step 4: Write TLS/PSD/evidence documents** Document current self-issued `.it` certificate and missing `.com` coverage; `TLS_CA_FILE`; `openssl x509 -noout -fingerprint -sha256`; out-of-band confirmation; renewal order; never bypass verification. PSD runbook mirrors Tasks 9–10 and excludes legacy test sessions/indexes/caches. Evidence contains public IDs, paths, modes, timestamps, checksums, statuses, owners, approvals only. - [ ] **Step 5: Link existing manuals** Link local/server workspace docs, PSD setup, user guide, docs index, MkDocs. Explicitly state `postgres_direct` and `ssh_tunnel` do not use these keys. - [ ] **Step 6: Prove GREEN** ```bash bash scripts/test-verify-dwh-auth-docs.sh bash scripts/verify-dwh-auth-docs.sh bash scripts/test-verify-workspace-install-docs.sh bash scripts/auth-docs-smoke.sh ``` - [ ] **Step 7: Commit** ```bash git add docs/install/dwh-auth-server.md docs/install/dwh-auth-client-enrollment.md docs/install/dwh-auth-tls.md docs/operations/psd-dwh-auth-rollout.md docs/testing/dwh-auth-manual-acceptance.md docs/testing/evidence/psd-dwh-auth-rollout-report-template.md scripts/verify-dwh-auth-docs.sh scripts/test-verify-dwh-auth-docs.sh docs/install/local-workspace-registry.md docs/install/server-workspace-registry.md docs/install/psd-workspace-setup.md docs/guida-utente.md docs/index.md mkdocs.yml git commit -m "docs: explain per-installation DWH access" ``` --- ### Task 8: Run source and portability checkpoint **Files:** - Modify only after a focused test fails: files from Tasks 1–7. - Record locally: `.artifacts/dwh-auth/source-verification.md`. **Interfaces:** - Consumes: complete candidate. - Produces: reviewed clean SHA eligible for PSD mutation gate. - [ ] **Step 1: Run focused gates** ```bash (cd tools/dwh-auth && go test -race ./... -count=1 && go vet ./...) bash scripts/test-dwh-auth-build-contract.sh bash scripts/test-dwh-auth-nginx-contract.sh bash scripts/test-dwh-auth-nginx-integration.sh bash scripts/test-dwh-auth-secret-scan.sh bash scripts/test-verify-dwh-auth-docs.sh ``` - [ ] **Step 2: Run portability regressions** ```bash bash scripts/test-default-compose.sh bash scripts/test-unified-compose.sh bash scripts/test-no-deployment-coupling-scope.sh bash scripts/test-compose-secret-policy.sh bash scripts/test-verify-workspace-install-docs.sh (cd tools/tht && go test ./... -count=1) ``` Required result: every command above passes; Compose remains unchanged; `tht` has no DWH-key command; Mac/Windows need no new binary. `test-no-deployment-coupling-scope.sh` is the required PASS regression gate for this candidate. `test-no-deployment-coupling.sh` is a separately tracked **BASELINE_RED** debt: it was already red at `4ef0a6a` because its global `\bpsd\b` prohibition scans approved PSD deployment/docs content. Do not modify that global gate in this work. Record both sanitized category/path-only outputs in `.artifacts/dwh-auth/source-verification.md`: `BASELINE_RED` for a detached `4ef0a6a` worktree and `CANDIDATE_RED` for this candidate. The candidate is expected to add intentional DWH manuals/bindings to that diagnostic output, so the two outputs are not expected to be identical. The runtime non-regression proof is instead the required empty immutable-path diff plus the scope regression PASS: ```bash git diff --exit-code 4ef0a6a -- \ compose.yaml \ deploy/compose.local.yaml \ deploy/compose.server.yaml \ scripts/run-stack.sh \ tools/tht ``` Record only the scanner category and relative path (never matching text) for the global-gate diagnostic. Its unrelated remediation remains future gate debt and is excluded from this Task 8 all-required-pass claim. - [ ] **Step 3: Check scope and leaks** ```bash git diff --check git status --short git log --oneline --decorate -8 bash scripts/test-dwh-auth-secret-scan.sh ``` Expected: whitespace clean; only intended evidence untracked; the non-printing credential-literal scan exits 0 only when there are zero full-format v1 matches; reviewed commits. Do not scan `legacy-shared.`: legacy credentials are opaque and that string can be a legitimate file path. - [ ] **Step 4: Terra review** Give Terra design, plan, diff, tests. Require requirement matrix and severity-ranked findings. Resolve Important/Critical with focused tests and commits; record Minor. - [ ] **Step 5: Freeze** ```bash git rev-parse HEAD git status --porcelain=v1 ``` Expected: full SHA recorded; execution worktree clean. Do not push/merge/install here. --- ### Task 9: Install on PSD without changing public Nginx **Files/objects:** - Install: `/usr/local/sbin/dwh-auth` - Install: `/etc/systemd/system/dwh-auth.service` - Install: `/usr/lib/tmpfiles.d/dwh-auth.conf` - Create: `/var/lib/dwh-auth/active/`, `/var/lib/dwh-auth/revoked/`, `/run/dwh-auth/verify.sock` - Protect: `/root/dwh-auth-provision/` - Evidence: owner-approved protected rollout directory **Interfaces:** - Consumes: frozen Task 8 SHA/binary. - Produces: healthy local authenticator with legacy and Mac identities, disconnected from Nginx. - [ ] **Step 1: Stop for explicit mutation authorization** Present SHA, results, exact targets, rollback, legacy-stack non-impact. Without explicit approval, stop with Activity 1 `IN_DISCUSSION`. - [ ] **Step 2: Verify preconditions read-only** Require `x86_64`, Nginx group `www-data`, free target names, systemd metadata, current `nginx -t`, current `/dwh/` diagnostic, and regular root-owned `0600` `/root/dwh-auth-provision/legacy-shared.key`. Never print/hash that key. - [ ] **Step 3: Build/fingerprint frozen binary** ```bash bash scripts/build-dwh-auth.sh --output /tmp/dwh-auth-release sha256sum /tmp/dwh-auth-release/dwh-auth-linux-amd64 ``` Record binary/source SHA only. - [ ] **Step 4: Install service** Create no-login group/user; install binary `0755`; install unit/tmpfiles `0644`; run `systemd-tmpfiles --create dwh-auth.conf`; verify owners/modes. Do not start until `check` passes. - [ ] **Step 5: Import legacy and issue Mac key** Import fixed protected legacy file with `--legacy-raw --installation-id legacy-shared`. Create non-expiring `psd-mac-primary` in root-owned `0600` `/root/dwh-auth-provision/psd-mac-primary.key`. Capture public metadata only. - [ ] **Step 6: Start/test disconnected service** Run `check`, `systemd-analyze verify`, daemon-reload, enable/start, verify socket. Via protected curl configs test new=204, legacy=204, random=401, absent=401 on Unix socket. Check bounded journal for absence of both keys. - [ ] **Step 7: Record checkpoint** Retain checksums, public IDs/modes, hardening/socket metadata, statuses, rollback. Do not touch Nginx. --- ### Task 10: Roll out dual-key Nginx and close Activity 1 **Files/objects:** - Create: `/etc/nginx/conf.d/dwh-auth-rate-limit.conf` - Modify: `/etc/nginx/sites-available/policlinicosandonato` - Preserve: protected pre-change copies - Update: Mac vault or protected `API_KEY_FILE` - Update: `docs/operations/psd-server-survey-remediation-checklist.md` - Complete: protected rollout evidence template **Interfaces:** - Consumes: Task 9 checkpoint, both protected keys, CA/fingerprint, explicit Nginx approval. - Produces: per-installation `/dwh/`, revoked legacy, verified Mac, Activity 1 PASS. - [ ] **Step 1: Stop for separate Nginx/client gate** Present files, backups, reload/rollback, delivery channel, observation duration, expected 204/401/503. Obtain explicit approval distinct from Task 9. - [ ] **Step 2: Prepare candidate** Create timestamped root-owned `0600` backups. Change only DWH auth plus rate map/zone. Preserve upstream `http://127.0.0.1:3001/`; keep vector locations byte-identical; clear API key before PostgREST; include no literal key. - [ ] **Step 3: Validate before reload** Run the structural checker and a secret scan that emits only PASS/FAIL metadata, install candidates, and run `sudo nginx -t`. Never run or retain a raw diff, `nginx -T`, or configuration dump: a legacy Nginx file can contain the exposed key. On failure restore backups before reload and record sanitized FAIL. - [ ] **Step 4: Reload/prove dual-key** Through real `.it` HTTPS and approved CA test `/dwh/rpc/ping` via file header curl protetti `0600`: pre-revoke v1=2xx, legacy=2xx, random=401, missing=401. Roll back on response/TLS/unrelated health change. - [ ] **Step 5: Deliver/configure Mac** Use approved protected channel. Verify CA fingerprint, configure vault or headless `API_KEY_FILE`, run **Validate workspace source**, **Test workspace connections**, `/rpc/ping`. Record public IDs, fingerprint confirmation, timestamp, result only. - [ ] **Step 6: Observe/revoke legacy** After approved window, revoke `legacy-shared` with reason `shared-credential-rotation`. New Mac still succeeds; legacy returns 401. Bounded logs contain no key/digest. - [ ] **Step 7: Verify rollback scope** Prove Nginx backups/commands readable. Do not preserve/migrate/restore/stop legacy ThothII data or stack. - [ ] **Step 8: Close only Activity 1** Complete sanitized evidence. Set PASS only with new success, legacy 401, service/Nginx PASS, clean logs, rollback, owner acceptance. Advance Current activity to 2; overall remains `SURVEY_NO_GO`. - [ ] **Step 9: Commit non-secret status** ```bash git add docs/operations/psd-server-survey-remediation-checklist.md PROJECT_STATE.md git commit -m "docs: record PSD DWH credential rotation" ``` Expected: public IDs/dates/evidence references/statuses only; no keys, digests, certificate body, connection string, raw log, or protected evidence. --- ## Final execution boundary Task 10 closes only Activity 1. It does not authorize Project A, stop legacy ThothII, or install the new application. Resume Activity 2 and remaining blockers before a fresh `SURVEY_GO`.