From 707c13d78180346515b68a3bac60ebf485127640 Mon Sep 17 00:00:00 2001 From: User Date: Fri, 21 Aug 2026 04:28:20 +0200 Subject: [PATCH] fix: harden DWH auth operator guidance --- deploy/psd/workspace-bindings.env.example | 5 +- docs/guida-utente.md | 8 +- docs/install/dwh-auth-client-enrollment.md | 13 ++- docs/install/dwh-auth-server.md | 102 ++++++++++++++++-- docs/install/dwh-auth-tls.md | 2 +- docs/install/local-workspace-registry.md | 12 +-- docs/install/psd-workspace-setup.md | 10 +- docs/install/server-workspace-registry.md | 8 +- docs/operations/psd-dwh-auth-rollout.md | 6 +- ...26-08-20-dwh-rest-per-installation-auth.md | 7 +- docs/testing/dwh-auth-manual-acceptance.md | 6 +- scripts/test-verify-dwh-auth-docs.sh | 39 ++++++- scripts/verify-dwh-auth-docs.sh | 28 +++-- scripts/verify-workspace-install-docs.sh | 6 +- 14 files changed, 196 insertions(+), 56 deletions(-) diff --git a/deploy/psd/workspace-bindings.env.example b/deploy/psd/workspace-bindings.env.example index b8754a2d..53d18cc5 100644 --- a/deploy/psd/workspace-bindings.env.example +++ b/deploy/psd/workspace-bindings.env.example @@ -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 diff --git a/docs/guida-utente.md b/docs/guida-utente.md index c108dc16..fe999e84 100644 --- a/docs/guida-utente.md +++ b/docs/guida-utente.md @@ -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. diff --git a/docs/install/dwh-auth-client-enrollment.md b/docs/install/dwh-auth-client-enrollment.md index 43bb4dfb..a91e4be9 100644 --- a/docs/install/dwh-auth-client-enrollment.md +++ b/docs/install/dwh-auth-client-enrollment.md @@ -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`. diff --git a/docs/install/dwh-auth-server.md b/docs/install/dwh-auth-server.md index 766e56c0..cded2651 100644 --- a/docs/install/dwh-auth-server.md +++ b/docs/install/dwh-auth-server.md @@ -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 --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 \ - --description \ - --output /root/dwh-auth-provision/-.key + --installation-id "$installation_id" \ + --description "$description" \ + --output "$key_output" ``` -Aggiungere `--expires-at ` 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 --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 | diff --git a/docs/install/dwh-auth-tls.md b/docs/install/dwh-auth-tls.md index 2ec5c775..d369be1b 100644 --- a/docs/install/dwh-auth-tls.md +++ b/docs/install/dwh-auth-tls.md @@ -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 diff --git a/docs/install/local-workspace-registry.md b/docs/install/local-workspace-registry.md index 27d26c31..9bf91723 100644 --- a/docs/install/local-workspace-registry.md +++ b/docs/install/local-workspace-registry.md @@ -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. @@ -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 diff --git a/docs/install/psd-workspace-setup.md b/docs/install/psd-workspace-setup.md index 3d1d7edd..7d730139 100644 --- a/docs/install/psd-workspace-setup.md +++ b/docs/install/psd-workspace-setup.md @@ -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`). diff --git a/docs/install/server-workspace-registry.md b/docs/install/server-workspace-registry.md index 17212ae9..1b2ef6a6 100644 --- a/docs/install/server-workspace-registry.md +++ b/docs/install/server-workspace-registry.md @@ -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. diff --git a/docs/operations/psd-dwh-auth-rollout.md b/docs/operations/psd-dwh-auth-rollout.md index 618bb3bd..22e1c2fd 100644 --- a/docs/operations/psd-dwh-auth-rollout.md +++ b/docs/operations/psd-dwh-auth-rollout.md @@ -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 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 index 3aebac0f..cd18298c 100644 --- 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 @@ -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** diff --git a/docs/testing/dwh-auth-manual-acceptance.md b/docs/testing/dwh-auth-manual-acceptance.md index fdff21fa..ccb70a68 100644 --- a/docs/testing/dwh-auth-manual-acceptance.md +++ b/docs/testing/dwh-auth-manual-acceptance.md @@ -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 | diff --git a/scripts/test-verify-dwh-auth-docs.sh b/scripts/test-verify-dwh-auth-docs.sh index 48d56bd7..589d69dc 100755 --- a/scripts/test-verify-dwh-auth-docs.sh +++ b/scripts/test-verify-dwh-auth-docs.sh @@ -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 diff --git a/scripts/verify-dwh-auth-docs.sh b/scripts/verify-dwh-auth-docs.sh index 36f85da7..f5cd2296 100755 --- a/scripts/verify-dwh-auth-docs.sh +++ b/scripts/verify-dwh-auth-docs.sh @@ -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 diff --git a/scripts/verify-workspace-install-docs.sh b/scripts/verify-workspace-install-docs.sh index 753369c4..0e33f703 100755 --- a/scripts/verify-workspace-install-docs.sh +++ b/scripts/verify-workspace-install-docs.sh @@ -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()