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
@@ -29,8 +29,7 @@ Authentik, Aritmolab o repository esterni.
- Current activity: `1`
- Title: Rotate or revoke the exposed DWH credential safely
- Resume from: Activity 1, review the written per-installation authentication design, then prepare
its executable implementation and rotation plan
- Resume from: Activity 1, review the approved implementation plan and choose its execution mode
- Discussion rule: una sola attività può essere `IN_DISCUSSION`
- Allowed states: `PENDING`, `IN_DISCUSSION`, `BLOCKED`, `PASS`
@@ -52,7 +51,7 @@ Authentik, Aritmolab o repository esterni.
| ID | Attività | Stato | Responsabile | Prossimo gate |
|---|---|---|---|---|
| 1 | Rotazione controllata della credenziale DWH esposta | `IN_DISCUSSION` | Proprietario del progetto | Revisionare la specifica scritta e approvare il piano eseguibile |
| 1 | Rotazione controllata della credenziale DWH esposta | `IN_DISCUSSION` | Proprietario del progetto | Revisionare il piano eseguibile e scegliere l’esecuzione |
| 2 | Assegnazione dei responsabili dei componenti condivisi | `PENDING` | Unassigned | Elenco owner confermato |
| 3 | Risoluzione del dominio pubblico `.it` oppure `.com` | `PENDING` | Unassigned | Origine autorevole documentata |
| 4 | Topologia e responsabilità del load balancer | `PENDING` | Unassigned | Route, health, TLS, rollback e allowlist verificati |
@@ -149,9 +148,12 @@ Authentik, Aritmolab o repository esterni.
- il proprietario conferma che le sessioni, gli indici e le cache del vecchio ThothII erano solo
test e non richiedono migrazione o attività di salvaguardia dedicate. Restano necessari il
rollback della route DWH condivisa e il rispetto del gate esplicito prima di fermare lo stack;
- il design approvato è scritto in
`docs/superpowers/specs/2026-08-20-dwh-rest-per-installation-auth-design.md` e richiede una
revisione del proprietario prima del piano di implementazione.
- il proprietario ha revisionato e approvato la specifica scritta. Il piano eseguibile è in
`docs/superpowers/plans/2026-08-20-dwh-rest-per-installation-auth.md`; separa sviluppo e
verifica del componente dai due gate espliciti di mutazione PSD;
- la credenziale condivisa corrente, priva del nuovo identificativo pubblico, sarà l’unico record
temporaneo `legacy_raw` con ID `legacy-shared`. Dopo la revoca non saranno accettate chiavi
prive del formato versionato per installazione.
- Decision: il nuovo ThothII server userà PostgreSQL diretto; il Mac e le future installazioni
remote useranno REST. La chiave condivisa corrente non è un modello finale accettabile: la
rotazione esposta userà una transizione a doppia credenziale e l'architettura target assegnerà
@@ -159,13 +161,12 @@ Authentik, Aritmolab o repository esterni.
trasporto. Il proprietario del progetto è accountable per coordinare la rotazione. Il meccanismo
target è il servizio server-only `dwh-auth`, indipendente dallo stack ThothII, con registro a file
e una chiave revocabile per installazione.
- Blockers: la specifica scritta attende la revisione del proprietario; mancano ancora il piano
eseguibile, la sorgente protetta per importare la chiave legacy, la procedura realizzata di
aggiornamento/verifica del Mac, la documentazione TLS completa, l'autorizzazione alla mutazione
- Blockers: il piano eseguibile attende la revisione del proprietario; mancano ancora la
sorgente protetta per importare la chiave legacy, la procedura realizzata di aggiornamento e
verifica del Mac, la documentazione TLS implementata, l'autorizzazione ai due gate di mutazione
e la revoca provata della vecchia chiave.
- Next step: dopo l'approvazione della specifica scritta, preparare il piano eseguibile di
implementazione e rotazione dual-key con test automatici, aggiornamento Mac, prova
positiva/negativa e rollback. Nessuna modifica Nginx avviene durante questa discussione.
- Next step: revisionare il piano eseguibile e scegliere tra esecuzione subagent-driven o inline.
Le Task 1–8 non mutano il server; le Task 9–10 richiedono due autorizzazioni esplicite separate.
## Activity 2: Identify accountable owners for shared components
@@ -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`.
@@ -2,7 +2,7 @@
**Date:** 2026-08-20
**Status:** Approved in discussion; awaiting review of this written specification
**Status:** Approved by the owner
## Purpose
@@ -105,10 +105,10 @@ beyond the approved PSD `systemd` deployment is not required by this implementat
One credential identifies one ThothII installation, not one human user. An installation may have
more than one temporarily active generation during rotation.
The external key has two components:
The external key contains a version marker, a public key ID, and a random secret:
```text
<public-key-id>.<random-secret>
thtdwh_v1.<public-key-id>.<random-secret>
```
Requirements:
@@ -271,7 +271,10 @@ is local-only and does not weaken the authentication decision.
The exposed shared credential is represented temporarily as `legacy-shared`. It is imported only
from an approved protected source and is never placed in a command argument, terminal output,
document, or evidence file.
document, or evidence file. Because the existing value does not use the new versioned key
format, it is stored as the only permitted `legacy_raw` record. The service compares the digest of
the complete opaque legacy header only for that reserved record. After `legacy-shared` is revoked,
no unversioned credential is accepted.
The production route does not switch to the new authenticator until all of the following hold: