diff --git a/docs/operations/psd-server-survey-remediation-checklist.md b/docs/operations/psd-server-survey-remediation-checklist.md index 7f5eb13c..32c30e5f 100644 --- a/docs/operations/psd-server-survey-remediation-checklist.md +++ b/docs/operations/psd-server-survey-remediation-checklist.md @@ -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 diff --git a/docs/superpowers/plans/2026-08-20-dwh-rest-per-installation-auth.md b/docs/superpowers/plans/2026-08-20-dwh-rest-per-installation-auth.md new file mode 100644 index 00000000..3aebac0f --- /dev/null +++ b/docs/superpowers/plans/2026-08-20-dwh-rest-per-installation-auth.md @@ -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= 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, 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`. diff --git a/docs/superpowers/specs/2026-08-20-dwh-rest-per-installation-auth-design.md b/docs/superpowers/specs/2026-08-20-dwh-rest-per-installation-auth-design.md index 2017bb2f..28373b2d 100644 --- a/docs/superpowers/specs/2026-08-20-dwh-rest-per-installation-auth-design.md +++ b/docs/superpowers/specs/2026-08-20-dwh-rest-per-installation-auth-design.md @@ -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 -. +thtdwh_v1.. ``` 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: