fix: harden DWH auth operator guidance

This commit is contained in:
User
2026-08-21 04:28:20 +02:00
parent 7b9b8b308d
commit 707c13d781
14 changed files with 196 additions and 56 deletions
+3 -2
View File
@@ -1,8 +1,9 @@
# Copia in un file operatore non tracciato (workspace-bindings.env).
# Esempio Mac/local/remota: copia in un file operatore non tracciato (workspace-bindings.env).
# Mai usare questo binding REST per il server PSD Project A: il server resta postgres_direct.
# Contiene SOLO bindings THT_WS_* non segreti. I *_FILE sono path DI CONTENITORE
# (/run/secrets/...), popolati dal connector override generato dai *_SOURCE dell'operatore env.
THT_WS_PSD_CLINICAL_DWH_TRANSPORT=rest_api
THT_WS_PSD_CLINICAL_DWH_BASE_URL=https://supabase-aritmolab.policlinicosandonato.it/dwh/
THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE=/run/secrets/psd-clinical-dwh-api-key
# Opzionale: solo se il DWH REST presenta una CA interna/privata.
# Obbligatorio per questo esempio Mac/local/remoto: il DWH REST PSD usa CA self-issued/private.
THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE=/run/secrets/psd-clinical-dwh-ca.pem
+4 -4
View File
@@ -179,14 +179,14 @@ revisione attiva e stato dell'ultimo aggiornamento.
**Livello 2 — workspace selezionato.** Questi comandi sono isolati perché richiedono prima la
selezione del workspace.
- **Validate workspace** verifica nuovamente catalogo, descrittore, Evidence e invarianti della
- **Validate workspace source** verifica nuovamente catalogo, descrittore, Evidence e invarianti della
revisione attiva selezionata. Non contatta il DWH e non modifica file.
- **Save runtime secrets** sostituisce alla cieca i valori compilati. I campi dipendono dal
- **Save entered secrets** sostituisce alla cieca i valori compilati. I campi dipendono dal
trasporto DWH e dall'autenticazione Evidence dichiarati; il backend restituisce solo lo stato
configurato/mancante.
- **Forget** elimina dal vault cifrato il singolo secret indicato. Le sessioni o operazioni future
- **Forget stored value** elimina dal vault cifrato il singolo secret indicato. Le sessioni o operazioni future
che lo richiedono restano bloccate finché non viene inserito di nuovo.
- **Test connections** materializza temporaneamente i secret necessari, contatta i servizi dati
- **Test workspace connections** materializza temporaneamente i secret necessari, contatta i servizi dati
configurati per quel workspace e rimuove i file temporanei alla fine. Non esporta né pubblica
nulla.
+6 -7
View File
@@ -23,18 +23,17 @@ pubblico.
1. In **Workspace management**, eseguire **Update workspace repository** se necessario e
selezionare il workspace.
2. Scegliere `rest_api`, controllare URL/trust locali e usare **Validate workspace**.
3. Inserire la chiave nel campo write-only **Data warehouse API key**, poi **Save runtime secrets**.
2. Il trasporto `rest_api` è una precondizione amministrativa del binding locale, non una scelta della GUI. Controllare URL/trust locali e usare **Validate workspace source**.
3. Inserire la chiave nel campo write-only **Data warehouse API key**, poi **Save entered secrets**.
La GUI la conserva nel vault cifrato `workspace-secrets`, non la rileggere né la restituisce.
4. Eseguire **Test connections**. Il controllo innocuo è `/rpc/ping`: atteso 2xx e
4. Eseguire **Test workspace connections**. Il controllo innocuo è `/rpc/ping`: atteso 2xx e
database/schema dichiarati.
5. Comunicare al server solo ID pubblico, timestamp e risultato. **Forget** rimuove il valore e va
5. Comunicare al server solo ID pubblico, timestamp e risultato. **Forget stored value** rimuove il valore e va
usato soltanto dopo conferma di sostituzione o revoca.
## Percorso headless: binding reale
`API_KEY_FILE` significa che il valore è nel file, non nella variabile. Nel file PSD non tracciato
`workspace-bindings.env` i binding effettivi sono:
`API_KEY_FILE` significa che il valore è nel file, non nella variabile. Questo è l'esempio Mac/local/remoto nel file PSD non tracciato `workspace-bindings.env`; non è il binding del server PSD Project A, che resta `postgres_direct`. I binding REST sono:
```dotenv
THT_WS_PSD_CLINICAL_DWH_TRANSPORT=rest_api
@@ -61,7 +60,7 @@ sostituire `PSD_CLINICAL` con ID immutabile maiuscolo (trattini in underscore).
## Ping, rotazione e revoca
Usare solo **Test connections** su `/rpc/ping`: successo è 2xx con TLS verificato; il server
Usare solo **Test workspace connections** su `/rpc/ping`: successo è 2xx con TLS verificato; il server
conferma l'ID con `key status`. Durante rotazione, ricevere nuova generazione, aggiornare vault o
file `API_KEY_FILE`, ripetere ping, attendere osservazione e far revocare la precedente. Dopo la
revoca: nuova positiva, precedente `401`.
+96 -6
View File
@@ -69,7 +69,8 @@ Usare sempre un root assoluto. Questi comandi espongono solo ID pubblici, stato,
```bash
sudo /usr/local/sbin/dwh-auth --registry-root /var/lib/dwh-auth check
sudo /usr/local/sbin/dwh-auth --registry-root /var/lib/dwh-auth key list --json
sudo /usr/local/sbin/dwh-auth --registry-root /var/lib/dwh-auth key status --key-id <public-key-id> --json
key_id=public-key-id
sudo /usr/local/sbin/dwh-auth --registry-root /var/lib/dwh-auth key status --key-id "$key_id" --json
```
Un errore di integrità, permessi, symlink o JSON malformato richiede ripristino da backup protetto,
@@ -81,13 +82,16 @@ Il comando crea la chiave una volta in un nuovo file assoluto `0600`; stdout con
pubblico, installazione e percorso. Il file di output non deve esistere.
```bash
installation_id=psd-mac-primary
description=operatore-mac-primario
key_output="/root/dwh-auth-provision/$installation_id-primary.key"
sudo /usr/local/sbin/dwh-auth --registry-root /var/lib/dwh-auth key create \
--installation-id <installation-id> \
--description <metadato-non-segreto> \
--output /root/dwh-auth-provision/<installation-id>-<generation>.key
--installation-id "$installation_id" \
--description "$description" \
--output "$key_output"
```
Aggiungere `--expires-at <RFC3339-UTC>` solo se la policy impone una scadenza; il default è nessuna
Aggiungere `--expires-at "YYYY-MM-DDTHH:MM:SSZ"` solo se la policy impone una scadenza; il default è nessuna
scadenza. Consegnare il file solo con vault aziendale, secret manager, MDM o trasferimento
autenticato ristretto. Mai email, chat, ticket, `cat` o copia-incolla. Il client conferma ID
pubblico e ping, poi il materiale temporaneo viene rimosso secondo policy.
@@ -106,8 +110,10 @@ l'ID pubblico; attendere l'osservazione; poi revocare la precedente e provare nu
precedente=401.
```bash
previous_key_id=public-key-id
revocation_reason=shared-credential-rotation
sudo /usr/local/sbin/dwh-auth --registry-root /var/lib/dwh-auth key revoke \
--key-id <previous-public-key-id> --reason <non-secret-reason>
--key-id "$previous_key_id" --reason "$revocation_reason"
```
La revoca non è annullabile e un ID revocato non si ricrea.
@@ -124,6 +130,90 @@ La disinstallazione richiede autorizzazione esplicita, client REST migrati/revoc
più necessario. Solo allora disabilitare l'unità; conservare registro e backup fino alla retention
approvata. Non inserire `dwh-auth` in Compose o in `tht start`/`tht stop`.
## Procedure riproducibili e secret-safe
Eseguire soltanto nel gate autorizzato. Le variabili seguenti contengono percorsi, timestamp e codici, mai una chiave. Il manifest e l'archivio del registro sono `0600`; l'archivio resta materiale riservato.
```bash
run_id=$(date -u +%Y%m%dT%H%M%SZ)
backup_root=/root/dwh-auth-provision
registry_backup="$backup_root/registry-$run_id.tar"
manifest="$backup_root/registry-$run_id.manifest"
sudo install -o root -g root -m 0600 /dev/null "$registry_backup"
sudo install -o root -g root -m 0600 /dev/null "$manifest"
sudo tar --acls --xattrs -C /var/lib -cf "$registry_backup" dwh-auth
sudo sh -c 'sha256sum "$1" > "$2"' sh "$registry_backup" "$manifest"
if sudo sha256sum -c "$manifest" >/dev/null; then printf 'registry_manifest=PASS\n'; else printf 'registry_manifest=FAIL\n' >&2; exit 1; fi
```
Per ripristinare, fermare prima il servizio, verificare il manifest senza stamparne il contenuto, estrarre l'archivio solo nel root autorizzato, rieseguire `dwh-auth check` e avviare l'unità. Conservare archivio e manifest per la retention approvata; non sovrascrivere né cancellare record per correggere un errore.
```bash
if ! sudo sha256sum -c "$manifest" >/dev/null; then printf 'registry_restore=FAIL\n' >&2; exit 1; fi
sudo systemctl stop dwh-auth
sudo tar --acls --xattrs -C /var/lib -xf "$registry_backup"
sudo /usr/local/sbin/dwh-auth --registry-root /var/lib/dwh-auth check
sudo systemctl start dwh-auth
printf 'registry_restore=PASS\n'
```
Per i test socket, creare la configurazione curl `0600` leggendo il file chiave direttamente nel file di configurazione: il valore non passa in argv, ambiente o stdout.
```bash
key_file=/root/dwh-auth-provision/psd-mac-primary.key
curl_cfg=/root/dwh-auth-provision/dwh-auth-new.curl
random_cfg=/root/dwh-auth-provision/dwh-auth-random.curl
sudo install -o root -g root -m 0600 /dev/null "$curl_cfg"
sudo install -o root -g root -m 0600 /dev/null "$random_cfg"
sudo sh -c '{ printf "%s" "header = X-API-Key: "; tr -d "\r\n" < "$1"; printf "\n"; } > "$2"' sh "$key_file" "$curl_cfg"
sudo sh -c 'printf "%s\n" "header = X-API-Key: invalid-test" > "$1"' sh "$random_cfg"
```
Il socket `/verify` deve restituire 204 per il file nuovo e 401 per configurazione casuale e richiesta senza header; stampare soltanto il codice.
```bash
status=$(sudo curl --config "$curl_cfg" --unix-socket /run/dwh-auth/verify.sock --output /dev/null --silent --show-error --write-out '%{http_code}' http://localhost/verify)
[ "$status" = 204 ] && printf 'socket_new=PASS\n' || { printf 'socket_new=FAIL\n' >&2; exit 1; }
status=$(sudo curl --config "$random_cfg" --unix-socket /run/dwh-auth/verify.sock --output /dev/null --silent --show-error --write-out '%{http_code}' http://localhost/verify)
[ "$status" = 401 ] && printf 'socket_random=PASS\n' || { printf 'socket_random=FAIL\n' >&2; exit 1; }
status=$(sudo curl --unix-socket /run/dwh-auth/verify.sock --output /dev/null --silent --show-error --write-out '%{http_code}' http://localhost/verify)
[ "$status" = 401 ] && printf 'socket_missing=PASS\n' || { printf 'socket_missing=FAIL\n' >&2; exit 1; }
```
Per HTTPS reale usare la configurazione protetta e la CA approvata contro `/dwh/rpc/ping`: PostgREST può restituire qualsiasi 2xx, non si pretende 204. La prova 401 usa solo configurazione casuale protetta.
```bash
ping_url=https://supabase-aritmolab.policlinicosandonato.it/dwh/rpc/ping
ca_file=/root/dwh-auth-provision/psd-dwh-ca.pem
status=$(sudo curl --config "$curl_cfg" --cacert "$ca_file" --connect-timeout 5 --max-time 15 --output /dev/null --silent --show-error --write-out '%{http_code}' "$ping_url")
case "$status" in 2??) printf 'https_new=PASS\n' ;; *) printf 'https_new=FAIL\n' >&2; exit 1 ;; esac
status=$(sudo curl --config "$random_cfg" --cacert "$ca_file" --connect-timeout 5 --max-time 15 --output /dev/null --silent --show-error --write-out '%{http_code}' "$ping_url")
[ "$status" = 401 ] && printf 'https_random=PASS\n' || { printf 'https_random=FAIL\n' >&2; exit 1; }
```
Per provare 503 in una finestra approvata, registrare l'orario, fermare temporaneamente l'unità, eseguire il ping con timeout e trap di ripristino; il comando deve stampare solo PASS/FAIL.
```bash
was_active=$(sudo systemctl is-active dwh-auth || true)
[ "$was_active" = active ] || { printf 'https_auth_down=FAIL\n' >&2; exit 1; }
restore_auth() { sudo systemctl start dwh-auth; }
trap restore_auth EXIT INT TERM
sudo systemctl stop dwh-auth
status=$(sudo curl --config "$random_cfg" --cacert "$ca_file" --connect-timeout 5 --max-time 15 --output /dev/null --silent --show-error --write-out '%{http_code}' "$ping_url" || true)
[ "$status" = 503 ] && printf 'https_auth_down=PASS\n' || { printf 'https_auth_down=FAIL\n' >&2; exit 1; }
sudo systemctl start dwh-auth
trap - EXIT INT TERM
```
Lo scan journal non salva righe grezze: emette solo PASS/FAIL.
```bash
since=$(date -u -d '15 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
if sudo journalctl -u dwh-auth --since "$since" --no-pager --output=cat | grep -E 'thtdwh_v1|secret_sha256' >/dev/null; then printf 'journal_secret_scan=FAIL\n' >&2; exit 1; else printf 'journal_secret_scan=PASS\n'; fi
```
Dopo rollback verificato e migrazione/revoca di ogni client REST, la disinstallazione resta condizionata all'approvazione: eseguire `sudo systemctl disable --now dwh-auth`, ma mantenere registro, backup e manifest protetti per la retention; non cancellarli durante il rollback.
## Troubleshooting
| Sintomo | Interpretazione e azione |
+1 -1
View File
@@ -34,7 +34,7 @@ THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE=/run/secrets/psd-clinical-dwh-ca.pem
```
Il file sorgente locale è collegato da file operatore non tracciato. Usare URL `.it`, poi
**Test connections** su `/rpc/ping`. Non disabilitare TLS e non usare `curl -k`.
**Test workspace connections** su `/rpc/ping`. Non disabilitare TLS e non usare `curl -k`.
## Rinnovo coordinato
+6 -6
View File
@@ -60,9 +60,9 @@ After the installation is started, use Workspace management from the authenticat
clone.
2. Confirm that the installation-owned `workspace-secrets` storage remains outside the source
repository and contains no credentials in the workspace descriptors.
3. Select the workspace and run **Validate workspace** to verify the active descriptor, catalog,
3. Select the workspace and run **Validate workspace source** to verify the active descriptor, catalog,
Evidence, annotations, and runtime bindings.
4. Run **Test connections** only with the approved read-only DWH/Evidence test configuration.
4. Run **Test workspace connections** only with the approved read-only DWH/Evidence test configuration.
Results are redacted and the workspace source remains unchanged.
<!-- workspace-descriptor-contract:start -->
@@ -118,7 +118,7 @@ and fast-forward candidate checkout. It does not copy anything to the user's com
### Chiavi DWH REST per installazione
Se il binding selezionato è `rest_api`, la chiave DWH è una credenziale per questa installazione e si salva nel vault tramite **Save runtime secrets** oppure in un file locale indicato da `API_KEY_FILE`. `postgres_direct` e `ssh_tunnel` non usano questa chiave. Per emissione, TLS, rotazione e verifica `/rpc/ping`, seguire [enrollment DWH REST](dwh-auth-client-enrollment.md).
Se il binding selezionato è `rest_api`, la chiave DWH è una credenziale per questa installazione e si salva nel vault tramite **Save entered secrets** oppure in un file locale indicato da `API_KEY_FILE`. `postgres_direct` e `ssh_tunnel` non usano questa chiave. Per emissione, TLS, rotazione e verifica `/rpc/ping`, seguire [enrollment DWH REST](dwh-auth-client-enrollment.md).
Open Workspace management after the first successful repository update.
@@ -127,13 +127,13 @@ Open Workspace management after the first successful repository update.
tests do.
3. Review the runtime fields derived from the selected DWH transport and Evidence authentication
mechanism.
4. Enter or rotate the required values and choose **Save runtime secrets**.
5. Run **Validate workspace** and then **Test connections**.
4. Enter or rotate the required values and choose **Save entered secrets**.
5. Run **Validate workspace source** and then **Test workspace connections**.
Secret fields are write-only. The GUI receives only configured/missing status. Values are
encrypted by the backend in the platform-neutral `workspace-secrets` volume. ThothII temporarily
materializes a restrictive file only while an existing file-oriented connector needs it, then
removes that file when the runtime lease ends. **Forget** deletes the selected encrypted value.
removes that file when the runtime lease ends. **Forget stored value** deletes the selected encrypted value.
The workspace YAML stays environment-independent: it declares connector mechanisms, not host
paths or credentials. Installation trust material such as a Git CA or `known_hosts` remains an
+6 -4
View File
@@ -2,16 +2,18 @@
Authentication acceptance is documented in the [manual authentication matrix](../testing/authentication-manual-acceptance.md).
Use generic OIDC with Authentik as the certified group catalog, map only the exact TOT Users and
TOT Admin groups, then run Workspace Validate, `tht auth check`, `tht auth check --interactive`,
and Workspace Test in that order. Browser callback E2E, native Windows execution, approved PSD
TOT Admin groups, then run **Validate workspace source**, `tht auth check`, `tht auth check --interactive`,
and **Test workspace connections** in that order. Browser callback E2E, native Windows execution, approved PSD
manual identities, external L2, and the two parked restore-lock preconditions remain pending the
Task 15/release gates.
Guida operativa per collegare ThothII al DWH di PSD con il nuovo sistema (registry Git + descriptor
v3 + `tht`).
## Stato attuale (2026-08-13)
## Stato storico Mac/local (2026-08-13)
> Questo stato è storico per Mac/local; il server PSD Project A usa binding separato `postgres_direct` read-only.
>
> Per la rotazione della credenziale DWH, fare riferimento al [runbook PSD](../operations/psd-dwh-auth-rollout.md): non autorizza modifiche finché i due gate non sono approvati. Il ThothII PSD server resta `postgres_direct`; il Mac e i client remoti usano `rest_api` con una chiave per installazione. `postgres_direct` e `ssh_tunnel` non usano chiavi `dwh-auth`.
- **Repository PSD pubblicato:** `https://github.com/mptyl/tht-workspace-psd` (privato), branch
@@ -22,7 +24,7 @@ v3 + `tht`).
- **Config operatore pronta** (file reali gitignored in `deploy/psd/`): `operator.env`,
`thothii-installation.yaml` e i secret d'installazione in `secrets/` (pi-auth, secret bundle,
chiave SSH, known_hosts). L'API key DWH va completata nella gestione Workspace ed è conservata
nel vault cifrato del backend. Nessuna CA: il DWH REST usa HTTPS pubblico.
nel vault cifrato del backend. Il certificato REST è self-issued/private: ogni Mac/local senza trust equivalente deve usare `TLS_CA_FILE` e verificare il fingerprint fuori banda, come in `docs/install/dwh-auth-tls.md`.
- **Stack avviato** (progetto `thothii-70417a3e30ea`, via `tht start`): `qdrant`, `embedding`
(con `qwen3-embedding:0.6b`), `core`, `frontend` sani. Il registry ha **clonato e attivato**
`psd-clinical` (stato `ready`).
+4 -4
View File
@@ -87,9 +87,9 @@ After repository activation, an authenticated user can:
1. Review the configured repository identity and update it without selecting a workspace.
2. Select a workspace to see the DWH/Evidence credential fields required by its connector modes.
3. Blind-save or rotate values; returned responses contain status only.
4. Run **Validate workspace source** and then test its configured connections.
5. Forget an obsolete value after dependent sessions and jobs have ended.
3. Blind-save or rotate values with **Save entered secrets**; returned responses contain status only.
4. Run **Validate workspace source** and then **Test workspace connections**.
5. Use **Forget stored value** for an obsolete value after dependent sessions and jobs have ended.
The backend encrypts values in `/data/workspace-secrets`, including the installation-specific
master key. The server profile persists that directory inside `THT_DATA_ROOT`; no workspace YAML
@@ -106,7 +106,7 @@ descriptors, Evidence paths, and cross-workspace invariants at one commit, then
the complete candidate. A rejected candidate never replaces the previous active snapshot. The
application-owned checkout and snapshots are read-only runtime state.
Validation proves descriptor and repository structure. **Test connections** additionally
Validation proves descriptor and repository structure. **Test workspace connections** additionally
materializes the current runtime secrets and contacts only the selected workspace's configured
DWH/Evidence endpoints. Failure does not modify or publish workspace source.
+3 -3
View File
@@ -27,9 +27,9 @@ Serve un secondo consenso: presentare file, backup, canale consegna Mac, osserva
1. Creare copie timestampate `root:root` `0600` di `/etc/nginx/sites-available/policlinicosandonato` e file coinvolti; non allegare configurazioni Nginx grezze alle evidenze.
2. Aggiungere solo `/etc/nginx/conf.d/dwh-auth-rate-limit.conf` e route DWH; preservare upstream `http://127.0.0.1:3001/`, mantenere byte-identiche le location vector e rimuovere la chiave prima di PostgREST.
3. Eseguire checker strutturale, scansione con solo esito, diff limitato e `sudo nginx -t`. Se uno fallisce, ripristinare backup prima di reload e registrare FAIL sanitizzato.
4. Dopo consenso fare reload, poi HTTPS `.it` con CA e config curl protette: legacy=204, nuova=204, casuale=401, assente=401 su `/dwh/rpc/ping`; guasto autenticatore=503, mai accesso permissivo.
5. Consegnare al Mac chiave e CA separatamente, verificare fingerprint fuori banda, configurare vault GUI o `API_KEY_FILE`, poi **Validate workspace** e **Test connections**.
3. Eseguire checker strutturale, scansione segreti con solo `PASS/FAIL` e metadati, installare candidati e `sudo nginx -t`. No raw diff: non eseguire o conservare raw diff, `nginx -T` o dump: il file legacy può contenere la chiave. Se uno fallisce, ripristinare backup prima di reload e registrare FAIL sanitizzato.
4. Dopo consenso fare reload, poi HTTPS `.it` con CA e config curl protette: legacy=2xx, nuova=2xx, casuale=401, assente=401 su `/dwh/rpc/ping`; guasto autenticatore=503, mai accesso permissivo.
5. Consegnare al Mac chiave e CA separatamente, verificare fingerprint fuori banda, configurare vault GUI o `API_KEY_FILE`, poi **Validate workspace source** e **Test workspace connections**.
6. Dopo osservazione revocare `legacy-shared` con ragione `shared-credential-rotation`; nuova=204, legacy=401 e journal limitato senza chiavi/digest.
## Rollback e chiusura
@@ -684,8 +684,9 @@ 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.
Run the structural checker and a secret scan that emits only PASS/FAIL metadata, install candidates,
and run `sudo nginx -t`. Never run or retain a raw diff, `nginx -T`, or configuration dump: a legacy
Nginx file can contain the exposed key. On failure restore backups before reload and record sanitized FAIL.
- [ ] **Step 4: Reload/prove dual-key**
@@ -696,7 +697,7 @@ 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,
run **Validate workspace source**, **Test workspace connections**, `/rpc/ping`. Record public IDs, fingerprint confirmation,
timestamp, result only.
- [ ] **Step 6: Observe/revoke legacy**
+3 -3
View File
@@ -1,6 +1,6 @@
# Collaudo manuale `dwh-auth`
Eseguire questo collaudo soltanto con consenso ai gate PSD e con dati sintetici. Non eseguire ora
Prima del rollout, i test sono sintetici e non usano dati clinici. Nel rollout reale si usano credenziali reali esclusivamente su `/rpc/ping`, senza acquisire risultati clinici. Non eseguire ora
mutazioni server e non registrare chiavi, digest, certificate body, output Nginx grezzo o risultati
clinici.
@@ -19,8 +19,8 @@ clinici.
| Socket locale | Config curl protetta, nuova/legacy | `204` durante dual-key | codice, unit/socket status |
| Negativo locale | Config casuale e richiesta senza header | `401` | codice, nessun valore header |
| Guasto controllato | Autenticatore/registro non disponibili nel test approvato | `503`, mai accesso | codice e rollback |
| HTTPS reale | `/dwh/rpc/ping` con CA approvata | `204`/2xx, TLS valido | ID, esito e approvazione fingerprint |
| Mac | **Validate workspace**, **Test connections** | Ping positivo | timestamp e stato GUI |
| HTTPS reale | `/dwh/rpc/ping` con CA approvata | qualsiasi `2xx`, TLS valido | ID, esito e approvazione fingerprint |
| Mac | **Validate workspace source**, **Test workspace connections** | Ping positivo | timestamp e stato GUI |
| Revoca | Chiave precedente dopo osservazione | `401`; nuova ancora positiva | ID pubblico e codici |
| Trasporti | Server PSD diretto e SSH diagnostico | nessuna chiave `dwh-auth` | trasporto selezionato |
+36 -3
View File
@@ -26,7 +26,7 @@ trap cleanup EXIT
temp_root=$(mktemp -d /tmp/thothii-dwh-auth-docs.XXXXXXXX) || report_fail fixture_root
fixture_root="$temp_root/fixture"
mkdir -p "$fixture_root/docs/install" "$fixture_root/docs/operations" \
"$fixture_root/docs/testing/evidence" "$fixture_root/scripts"
"$fixture_root/docs/testing/evidence" "$fixture_root/scripts" "$fixture_root/deploy/psd"
for relative in \
docs/install/dwh-auth-server.md \
@@ -45,6 +45,7 @@ for relative in \
cp "$root/$relative" "$fixture_root/$relative"
done
cp -a "$root/docs/." "$fixture_root/docs/"
cp "$root/deploy/psd/workspace-bindings.env.example" "$fixture_root/deploy/psd/workspace-bindings.env.example"
"$verify" --root "$fixture_root" || report_fail positive_source
report_pass positive_source
@@ -65,13 +66,45 @@ fake_key="thtdwh_v1.$(printf 'A%.0s' {1..16}).$(printf 'A%.0s' {1..43})"
fake_digest="$(printf 'A%.0s' {1..43})"
expect_rejected credential_literal docs/install/dwh-auth-client-enrollment.md "$fake_key"
expect_rejected credential_digest_literal docs/testing/evidence/psd-dwh-auth-rollout-report-template.md "secret_sha256: $fake_digest"
expect_rejected opaque_legacy_header_literal docs/install/dwh-auth-client-enrollment.md "curl --header 'X-API-Key: opaque-legacy-fixture' https://example.invalid/dwh/rpc/ping"
expect_rejected credential_digest_literal docs/testing/evidence/psd-dwh-auth-rollout-report-template.md "\"secret_sha256\": \"$fake_digest\""
expect_rejected curl_insecure docs/install/dwh-auth-tls.md 'curl -k https://example.invalid/dwh/rpc/ping'
expect_rejected tls_disabled docs/install/dwh-auth-tls.md 'verify_tls=false'
expect_rejected secret_in_environment docs/install/dwh-auth-client-enrollment.md "DWH_API_KEY=$fake_key"
expect_rejected secret_in_environment docs/install/dwh-auth-client-enrollment.md "export THT_WS_PSD_CLINICAL_DWH_API_KEY=$fake_key"
expect_rejected secret_in_argv docs/install/dwh-auth-client-enrollment.md "curl -H 'X-API-Key: $fake_key' https://example.invalid/dwh/rpc/ping"
expect_rejected world_readable_secret docs/install/dwh-auth-server.md 'chmod 0644 /root/dwh-auth-provision/client.key'
expect_rejected sudo_raw_nginx_capture docs/operations/psd-dwh-auth-rollout.md 'sudo nginx -T > /tmp/nginx-full.conf'
expect_rejected raw_diff_capture docs/operations/psd-dwh-auth-rollout.md 'sudo diff -u /etc/nginx/sites-available/policlinicosandonato /root/backup.conf'
expect_rejected git_raw_diff_capture docs/operations/psd-dwh-auth-rollout.md 'git diff --no-index /root/old.conf /root/new.conf'
expect_rejected raw_nginx_capture docs/operations/psd-dwh-auth-rollout.md 'nginx -T > /tmp/nginx-full.conf'
expect_rejected compose_coupling docs/install/dwh-auth-server.md 'docker compose up dwh-auth'
expect_replacement_rejected() {
local name=$1 target=$2 needle=$3 replacement=$4
local case_root="$temp_root/$name"
cp -a "$fixture_root" "$case_root"
[[ $(grep -Foc -- "$needle" "$case_root/$target") -eq 1 ]] || report_fail "${name}_fixture"
sed -i "s~$needle~$replacement~" "$case_root/$target"
if "$verify" --root "$case_root" >/dev/null 2>&1; then report_fail "$name"; fi
report_pass "$name"
}
expect_global_replacement_rejected() {
local name=$1 target=$2 needle=$3 replacement=$4
local case_root="$temp_root/$name"
cp -a "$fixture_root" "$case_root"
[[ $(grep -Foc -- "$needle" "$case_root/$target") -gt 0 ]] || report_fail "${name}_fixture"
sed -i "s~$needle~$replacement~g" "$case_root/$target"
if "$verify" --root "$case_root" >/dev/null 2>&1; then report_fail "$name"; fi
report_pass "$name"
}
expect_replacement_rejected missing_exact_gui_label docs/install/dwh-auth-client-enrollment.md "Validate workspace source" "Validate workspace"
expect_replacement_rejected server_transport_contradiction docs/operations/psd-dwh-auth-rollout.md 'server PSD: `postgres_direct` read-only' 'server PSD: `rest_api` read-only'
expect_replacement_rejected missing_mac_local_marker deploy/psd/workspace-bindings.env.example "Mac/local/remota" "server PSD"
expect_replacement_rejected missing_private_ca docs/install/psd-workspace-setup.md "TLS_CA_FILE" "TLS_CA_REMOVED"
expect_global_replacement_rejected missing_socket_path docs/install/dwh-auth-server.md "/run/dwh-auth/verify.sock" "/run/dwh-auth/other.sock"
expect_replacement_rejected rest_transport_flag deploy/psd/workspace-bindings.env.example "THT_WS_PSD_CLINICAL_DWH_TRANSPORT=rest_api" "THT_WS_PSD_CLINICAL_DWH_TRANSPORT=postgres_direct"
report_pass summary
+20 -8
View File
@@ -24,6 +24,7 @@ docs = {
"guide": "docs/guida-utente.md",
"index": "docs/index.md",
"nav": "mkdocs.yml",
"psd_template": "deploy/psd/workspace-bindings.env.example",
}
text = {}
for label, relative in docs.items():
@@ -33,11 +34,11 @@ for label, relative in docs.items():
text[label] = path.read_text(encoding="utf-8")
requirements = {
"server": ["/var/lib/dwh-auth", "root:dwh-auth", "2750", ".writer.lock", "0640", "/run/dwh-auth/verify.sock", "0660", "systemd", "key create", "key list", "key status", "key revoke", "check", "backup", "rollback", "disinstallazione", "rest_api", "postgres_direct", "ssh_tunnel"],
"client": ["Workspace management", "Save runtime secrets", "API_KEY_FILE", "THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE", "TLS_CA_FILE", "/rpc/ping", "rest_api", "postgres_direct", "ssh_tunnel", "401", "503", "rotazione", "revoca"],
"server": ["manifest", "curl_cfg", "journalctl", "systemctl disable --now", "trap", "/var/lib/dwh-auth", "root:dwh-auth", "2750", ".writer.lock", "0640", "/run/dwh-auth/verify.sock", "0660", "systemd", "key create", "key list", "key status", "key revoke", "check", "backup", "rollback", "disinstallazione", "rest_api", "postgres_direct", "ssh_tunnel"],
"client": ["Workspace management", "Validate workspace source", "Test workspace connections", "Save entered secrets", "Forget stored value", "API_KEY_FILE", "THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE", "TLS_CA_FILE", "/rpc/ping", "rest_api", "postgres_direct", "ssh_tunnel", "401", "503", "rotazione", "revoca"],
"tls": ["self-issued", ".it", ".com", "SAN", "TLS_CA_FILE", "openssl x509 -noout -fingerprint -sha256", "fuori banda", "rinnovo", "curl -k"],
"rollout": ["Task 9", "Task 10", "IN_DISCUSSION", "postgres_direct", "rest_api", "legacy-shared", "nginx -t", "204", "401", "503", "Qdrant", "Ollama", "rollback"],
"manual": ["204", "401", "503", "TLS", "registry", "postgres_direct", "ssh_tunnel"],
"rollout": ["PASS/FAIL", "no raw diff", "Task 9", "Task 10", "IN_DISCUSSION", "postgres_direct", "rest_api", "legacy-shared", "nginx -t", "204", "401", "503", "Qdrant", "Ollama", "rollback"],
"manual": ["credenziali reali", "sintetici", "/rpc/ping", "204", "401", "503", "TLS", "registry", "postgres_direct", "ssh_tunnel"],
"evidence": ["ID pubblici", "owner", "mode", "timestamp", "checksum", "approvazione"],
}
for label, tokens in requirements.items():
@@ -64,16 +65,27 @@ for relative in (docs["server"], docs["client"], docs["tls"], docs["rollout"], d
corpus = "\n".join(text.values())
for pattern, label in [
(r"thtdwh_v1\.[A-Za-z0-9_-]{16}\.[A-Za-z0-9_-]{43}", "credential literal"),
(r"(?mi)^\s*secret_sha256\s*[:=]\s*[A-Za-z0-9_-]{16,}", "credential digest literal"),
(r"(?mi)^\s*[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD)\s*=\s*(?!/|<)[^\s#]+", "secret in environment"),
(r"(?i)(?:curl|dwh-auth)[^\n]{0,240}(?:-H\s+['\"][^'\"]*X-API-Key\s*:|--(?:api-key|token|password)\b)", "secret in argv"),
(r"(?mi)^\s*[\"']?secret_sha256[\"']?\s*[:=]\s*[\"']?[A-Za-z0-9_-]{16,}", "credential digest literal"),
(r"(?mi)^\s*(?:export\s+)?[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD)\s*=\s*(?!/|<)[^\s#]+", "secret in environment"),
(r"(?i)(?:curl|dwh-auth)[^\n]{0,240}(?:(?:-H|--header)\s+['\"][^'\"]*X-API-Key\s*:|--(?:api-key|token|password)\b)", "secret in argv"),
(r"(?im)^(?!.*(?:non usare|mai usare)).*curl\s+(?:[^\n]*\s)?(?:-k|--insecure)\b|verify_tls\s*=\s*false|insecure_skip_verify", "TLS bypass"),
(r"(?i)chmod\s+0?[0-7][0-7][4-7]\s+[^\n]*(?:\.key|secret|provision)", "world-readable secret"),
(r"(?m)^\s*nginx\s+-T\b", "raw Nginx capture"),
(r"(?m)^\s*(?:sudo\s+)?nginx\s+-T\b", "raw Nginx capture"),
(r"(?m)^\s*(?:sudo\s+)?(?:diff\b|git\s+diff\b)", "raw diff capture"),
(r"(?i)docker\s+compose[^\n]*\bdwh-auth\b", "Compose coupling"),
]:
if re.search(pattern, corpus):
raise SystemExit(f"dwh-auth docs: forbidden {label}")
server_direct = re.search(r"server PSD[^\n]{0,100}postgres_direct", text["rollout"], re.I)
psd_direct = re.search(r"server PSD[^\n]{0,100}postgres_direct", text["psd"], re.I)
if not server_direct or not psd_direct:
raise SystemExit("dwh-auth docs: PSD server must remain postgres_direct")
template = text["psd_template"]
if "tht_ws_psd_clinical_dwh_transport=rest_api" not in template.lower() or "mac/local/remota" not in template.lower() or not re.search(r"mai .*server psd", template, re.I):
raise SystemExit("dwh-auth docs: PSD REST template must be explicitly Mac/local/remota, never server PSD")
if "TLS_CA_FILE" not in text["psd"] or re.search(r"(?i)nessuna CA|HTTPS pubblico", text["psd"]):
raise SystemExit("dwh-auth docs: PSD setup contradicts private CA TLS requirement")
print("dwh-auth documentation contract passed")
PY
+4 -2
View File
@@ -517,8 +517,10 @@ flow_tokens = [
"ThothII",
"Update workspace repository",
"workspace-secrets",
"Validate workspace",
"Test connections",
"Validate workspace source",
"Test workspace connections",
"Save entered secrets",
"Forget stored value",
]
for guide in (local_path, server_path):
text = guide.read_text()