Files
ThothII/docs/superpowers/plans/2026-08-20-dwh-rest-per-installation-auth.md
T

761 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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=<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, 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`.