docs: plan per-installation DWH REST auth

This commit is contained in:
User
2026-08-20 22:44:23 +02:00
parent 8a1c23536f
commit 4ef0a6a833
3 changed files with 752 additions and 16 deletions
@@ -0,0 +1,732 @@
# 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
go list -deps ./... | grep -v '^github.com/aritmolab/thothii/tools/dwh-auth' | grep '\.'
```
Expected: tests pass; dependency scan exits 1 with no third-party path.
- [ ] **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=<id> installation_id=<id> output=<path>`.
- [ ] **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, clears `X-API-Key` before PostgREST, burst
100, preserves `/dwh/`, 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-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-no-deployment-coupling.sh
bash scripts/test-compose-secret-policy.sh
bash scripts/test-verify-workspace-install-docs.sh
(cd tools/tht && go test ./... -count=1)
```
Expected: all pass; Compose unchanged; `tht` has no DWH-key command; Mac/Windows need no new binary.
- [ ] **Step 3: Check scope and leaks**
```bash
git diff --check
git status --short
git log --oneline --decorate -8
rg -n --hidden --glob '!.git/**' --glob '!docs/superpowers/**' 'legacy-shared\.[A-Za-z0-9_-]|thtdwh_v1\.[A-Za-z0-9_-]{16}\.[A-Za-z0-9_-]{43}' .
```
Expected: whitespace clean; only intended evidence untracked; secret scan exits 1; reviewed commits.
- [ ] **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 structural checker, secret scan without match output, bounded diff, install candidates,
`sudo nginx -t`. 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 protected curl configs:
legacy success, new success, random 401, absent 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 Workspace Validate, Test 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`.