docs: plan per-installation DWH REST auth
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user