This commit is contained in:
@@ -75,7 +75,6 @@ This section applies only when a Linux `profile: server` descriptor declares a r
|
||||
The canonical authentication root stays root-owned and is the only authority. The container reads
|
||||
only the separate read-only runtime projection selected by `CURRENT`; it never falls back to the
|
||||
canonical files or to a previous generation. Run projected mutations and repairs through the
|
||||
root-operated `tht` commands documented in the [server guide](server.md), and never edit runtime
|
||||
files directly.
|
||||
root-operated `tht` commands, and never edit runtime files directly.
|
||||
|
||||
Mac, Windows, and local direct-file authentication remain unchanged when the projection is absent.
|
||||
|
||||
@@ -71,7 +71,7 @@ The surfaces have distinct semantics and this order is recommended:
|
||||
issuer/JWKS, catalog credentials, and all configured mapped groups.
|
||||
3. `tht auth check --interactive` repeats live diagnosis and additionally validates a device-flow
|
||||
identity and its direct `groups` claim when Device Authorization is available.
|
||||
4. Workspace Test performs aggregate live workspace and authentication validation.
|
||||
4. Installation diagnostics perform aggregate live workspace and authentication validation.
|
||||
|
||||
The live CLI forms are:
|
||||
|
||||
@@ -87,8 +87,8 @@ real ID token including `groups`. It is an operator check, not a replacement for
|
||||
`tht doctor` emits this exact ordered report: `descriptor`, `files`, `docker`, `compose`,
|
||||
`configuration`, `authentication`, `services`, `core-http`, `frontend-http`,
|
||||
`workspace-registry`, `workflow`, `pi`. Its authentication entry is live and non-interactive.
|
||||
Any authentication failure makes Workspace Validate or Workspace Test non-activatable according
|
||||
to that surface's static or live scope.
|
||||
Any authentication failure prevents activation according to the static or live scope of the
|
||||
relevant diagnostic surface.
|
||||
|
||||
The complete closed diagnostic-code union and exact role-to-permission expansion are in the
|
||||
[authentication architecture](../architecture/authentication.md).
|
||||
|
||||
+35
-31
@@ -1,37 +1,41 @@
|
||||
# Authentik provider setup
|
||||
# Authentik provider configuration
|
||||
|
||||
Authentik is the first certified provider for PSD acceptance. The ThothII browser protocol remains
|
||||
generic OIDC; these steps configure the provider-specific group catalog only.
|
||||
ThothII uses generic OIDC in the browser. Authentik provides the identity provider and group
|
||||
catalog without adding a proprietary login flow.
|
||||
|
||||
1. Create an OAuth2/OIDC application and provider in Authentik. Register exactly
|
||||
`<publicUrl>/api/auth/oidc/callback` as the callback and enable `openid`, `profile`, and `email`.
|
||||
2. Configure the provider so the ID token contains a direct `groups` array of strings. Verify the
|
||||
claim with a disposable test identity before running acceptance.
|
||||
3. Create a dedicated API service account for the group catalog. Grant group-view-only privilege;
|
||||
do not grant write, user-management, or directory-administration privilege. Put its bearer value
|
||||
in the protected bundle under `THT_AUTHENTIK_API_TOKEN`.
|
||||
4. Create or confirm the exact groups `TOT Users` and `TOT Admin`. Map them explicitly in
|
||||
`auth.yaml` to `user` and `admin`, respectively. Keep other upstream groups out of the mapping.
|
||||
5. Run Workspace Validate for static authentication validation. Then run live non-interactive
|
||||
diagnosis, followed by the optional device-flow identity check:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Browser
|
||||
participant ThothII
|
||||
participant Authentik
|
||||
Browser->>ThothII: Sign in
|
||||
ThothII->>Authentik: Authorization Code with PKCE
|
||||
Authentik-->>Browser: Login and consent
|
||||
Browser->>ThothII: Callback with code
|
||||
ThothII->>Authentik: Token exchange
|
||||
Authentik-->>ThothII: Identity and groups
|
||||
ThothII-->>Browser: Opaque session
|
||||
```
|
||||
|
||||
```sh
|
||||
tht auth check
|
||||
tht auth check --interactive
|
||||
tht doctor --json
|
||||
```
|
||||
## OIDC provider
|
||||
|
||||
6. Run Workspace Test for aggregate live workspace and authentication validation. It must prove
|
||||
discovery/JWKS, catalog access, and every configured group. The diagnostic result must contain
|
||||
no secret values. `tht doctor --json` reports `authentication` after `configuration` and before
|
||||
`services` in its exact ordered checklist.
|
||||
1. Create an OAuth2/OIDC application and provider.
|
||||
2. Register exactly `PUBLIC_URL/api/auth/oidc/callback`.
|
||||
3. Enable the `openid`, `profile`, and `email` scopes.
|
||||
4. Configure a direct `groups` claim as an array of strings.
|
||||
|
||||
Only configured exact group names are queried. Additional Authentik or directory groups are ignored
|
||||
silently, without a warning. A mapped group absent from Authentik fails closed with
|
||||
`oidc_mapped_group_missing`; an ambiguous exact-name result uses
|
||||
`oidc_mapped_group_ambiguous`. A group visible only in an upstream directory but not represented
|
||||
in Authentik is missing from ThothII’s catalog and must not be treated as present.
|
||||
## Group catalog
|
||||
|
||||
Rotate the two credentials independently through the protected secret-file procedure, then repeat
|
||||
`tht auth check` and workspace Test. Never put either value in this guide, YAML, shell history,
|
||||
diagnostic output, or acceptance evidence.
|
||||
Create a dedicated service account with read-only access to groups. Store its token in the
|
||||
protected bundle as `THT_AUTHENTIK_API_TOKEN`.
|
||||
|
||||
Map the exact enterprise group names to the ThothII `user` and `admin` roles in `auth.yaml`.
|
||||
Unmapped groups are ignored. A configured group that does not exist produces a closed error.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
`tht auth check` checks discovery, the issuer, JWKS, catalog access, and the configured groups.
|
||||
The `--interactive` option also verifies identity through device flow when the provider supports it.
|
||||
|
||||
Rotate the OIDC secret and group-catalog token separately. Neither may appear in YAML, shell
|
||||
history, logs, or diagnostic output.
|
||||
|
||||
@@ -1,70 +1,39 @@
|
||||
# Enrollment client per DWH REST
|
||||
# DWH REST client enrollment
|
||||
|
||||
La credenziale `dwh-auth` appartiene a una installazione ThothII, non a una persona. Serve solo se
|
||||
il trasporto è `rest_api`; `postgres_direct` e `ssh_tunnel` non la usano.
|
||||
The `dwh-auth` credential belongs to one ThothII installation and is needed only when the
|
||||
workspace uses the `rest_api` transport.
|
||||
|
||||
| Trasporto | Chiave `dwh-auth` | Materiale locale |
|
||||
| --- | --- | --- |
|
||||
| `rest_api` | Sì, una per installazione. | URL HTTPS, `API_KEY_FILE`, eventuale `TLS_CA_FILE`. |
|
||||
| `postgres_direct` | No. | Credenziali PostgreSQL e TLS PostgreSQL. |
|
||||
| `ssh_tunnel` | No. | Credenziali PostgreSQL e materiali SSH; è diagnostico-only nel runtime corrente. |
|
||||
| Trasporto | Materiale richiesto |
|
||||
| --- | --- |
|
||||
| `rest_api` | URL HTTPS, `API_KEY_FILE`, eventuale `TLS_CA_FILE` |
|
||||
| `postgres_direct` | Credenziali PostgreSQL e configurazione TLS PostgreSQL |
|
||||
| `ssh_tunnel` | Credenziali PostgreSQL e materiale SSH |
|
||||
|
||||
Il ThothII server PSD resta `postgres_direct` read-only. Il Mac PSD e le installazioni remote
|
||||
usano `rest_api`; non introdurre un tunnel SSH per aggirare REST.
|
||||
## Delivery and storage
|
||||
|
||||
## Prerequisiti
|
||||
Receive the key and CA through separate protected channels. Store the key in the installation
|
||||
vault or in a regular file accessible only to the authorized account. Do not put it in Git, YAML
|
||||
files, arguments, logs, or shared screens.
|
||||
|
||||
Ricevere chiave e CA, se necessaria, attraverso canali protetti separati. Confermare fuori banda il
|
||||
fingerprint TLS prima dell'uso: [guida TLS](dwh-auth-tls.md). Conservare la chiave nel vault o in
|
||||
un file protetto, mai Git, `.env` con il valore, argv, ambiente, log o evidenze. Annotare solo ID
|
||||
pubblico.
|
||||
## ACME Limited configuration
|
||||
|
||||
## Percorso GUI: vault dell'installazione
|
||||
|
||||
1. In **Workspace management**, eseguire **Update workspace repository** se necessario e
|
||||
selezionare il workspace.
|
||||
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 workspace connections**. Il controllo innocuo è `/rpc/ping`: atteso 2xx e
|
||||
database/schema dichiarati.
|
||||
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. 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:
|
||||
Esempio di binding headless per il workspace `acme-ebikes`:
|
||||
|
||||
```dotenv
|
||||
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
|
||||
THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE=/run/secrets/psd-clinical-dwh-ca.pem
|
||||
THT_WS_ACME_EBIKES_DWH_TRANSPORT=rest_api
|
||||
THT_WS_ACME_EBIKES_DWH_BASE_URL=https://dwh.acme.example/dwh/
|
||||
THT_WS_ACME_EBIKES_DWH_API_KEY_FILE=/run/secrets/acme-ebikes-dwh-api-key
|
||||
THT_WS_ACME_EBIKES_DWH_TLS_CA_FILE=/run/secrets/acme-ebikes-dwh-ca.pem
|
||||
```
|
||||
|
||||
Nel file `operator.env` non tracciato, ogni suffisso `_SOURCE` indica solo il percorso assoluto del
|
||||
file protetto di origine. Il comando genera un override non tracciato che monta quei file nel
|
||||
`core`; non installa né avvia `dwh-auth` con Compose:
|
||||
The workspace suffix comes from the immutable ID, with hyphens changed to underscores and letters
|
||||
converted to uppercase. `API_KEY_FILE` contains the mounted file path, not the key value.
|
||||
|
||||
```bash
|
||||
bash scripts/generate-connector-secrets-override.sh \
|
||||
--bindings-env /absolute/protected/workspace-bindings.env \
|
||||
--operator-env /absolute/protected/operator.env \
|
||||
--output /absolute/protected/connector-secrets.override.yaml \
|
||||
--service core --role dwh
|
||||
```
|
||||
## Rotation and revocation
|
||||
|
||||
La chiave sorgente è un file regolare `0600` per il solo account autorizzato. Per altri workspace,
|
||||
sostituire `PSD_CLINICAL` con ID immutabile maiuscolo (trattini in underscore). Vedere anche il
|
||||
[protocollo diagnostico](../workspace-diagnostic-protocol.md).
|
||||
During rotation, receive the new generation, update the vault or mounted file, and confirm
|
||||
connectivity through the harmless `/rpc/ping` route. The server owner revokes the previous
|
||||
generation only after this confirmation.
|
||||
|
||||
## Ping, rotazione e revoca
|
||||
|
||||
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`.
|
||||
|
||||
`401` non distingue chiave assente, scaduta o revocata. `503` è un guasto fail-closed di servizio,
|
||||
socket o registro: non usare connessione diretta e non ridurre TLS. Non riattivare una chiave
|
||||
revocata. Il percorso PSD è nel [runbook](../operations/psd-dwh-auth-rollout.md).
|
||||
A `401` means the key is missing, unknown, expired, or revoked. A `503` means the authorization
|
||||
service or registry is unavailable. In either case, do not bypass REST or weaken TLS verification.
|
||||
|
||||
+53
-346
@@ -1,356 +1,63 @@
|
||||
# `dwh-auth`: guida server
|
||||
# `dwh-auth`: server guide
|
||||
|
||||
`dwh-auth` autentica la route REST `/dwh/` con una chiave per installazione. È un componente Linux
|
||||
opzionale e server-side: usa `systemd`, non `tht` né Docker Compose, non legge risultati clinici e
|
||||
non si collega a PostgreSQL. La chiave serve solo a `rest_api`; `postgres_direct` e `ssh_tunnel`
|
||||
non la usano.
|
||||
`dwh-auth` protects the REST `/dwh/` route with a separate key for each ThothII installation.
|
||||
It runs as a separate Linux service, does not read DWH data, and does not connect directly to
|
||||
PostgreSQL.
|
||||
|
||||
## Prerequisiti e confini
|
||||
|
||||
- Usare un checkout revisionato, Docker per la build e un operatore autorizzato sul server DWH.
|
||||
- Una chiave identifica un'installazione, non una persona. L'`installation-id` è unico, non
|
||||
personale e senza dati clinici.
|
||||
- Chiavi, digest, file di consegna e backup restano in file protetti: mai Git, argv, variabili
|
||||
d'ambiente, log, JSON pubblico o evidenze.
|
||||
- Preparare backup e rollback prima di Nginx. Installare il servizio non autorizza una modifica
|
||||
della route pubblica.
|
||||
|
||||
## Percorsi, owner e mode
|
||||
|
||||
| Oggetto | Percorso | Owner e mode |
|
||||
| --- | --- | --- |
|
||||
| Binario | `/usr/local/sbin/dwh-auth` | `root:root`, `0755` |
|
||||
| Unit | `/etc/systemd/system/dwh-auth.service` | `root:root`, `0644` |
|
||||
| Tmpfiles | `/usr/lib/tmpfiles.d/dwh-auth.conf` | `root:root`, `0644` |
|
||||
| Registro, `active`, `revoked` | `/var/lib/dwh-auth/` | `root:dwh-auth`, `2750` |
|
||||
| Lock | `/var/lib/dwh-auth/.writer.lock` | `root:dwh-auth`, `0640` |
|
||||
| Record | `/var/lib/dwh-auth/{active,revoked}/<public-key-id>.json` | `root:dwh-auth`, `0640` |
|
||||
| Socket runtime | `/run/dwh-auth/verify.sock` | `dwh-auth:www-data`, `0660` |
|
||||
| Consegne e backup | `/root/dwh-auth-provision/` | directory `root:root` `0700`, file `0600` |
|
||||
|
||||
Il record conserva un digest interno (`secret_sha256`) e metadati, mai la chiave in chiaro. Non
|
||||
leggere, stampare, calcolare o mettere quel digest in una prova operativa.
|
||||
|
||||
## Build, installazione e avvio
|
||||
|
||||
Costruire dal commit congelato e registrare solo checksum del binario e SHA sorgente:
|
||||
|
||||
```bash
|
||||
cd /srv/thothii/app
|
||||
bash scripts/build-dwh-auth.sh --output /tmp/dwh-auth-release
|
||||
sha256sum /tmp/dwh-auth-release/dwh-auth-linux-amd64
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CLIENT["Installazione ThothII"] -->|"X-API-Key"| NGINX["Nginx"]
|
||||
NGINX --> AUTH["dwh-auth\nUnix socket"]
|
||||
AUTH --> REGISTRY["Registro chiavi\nactive e revoked"]
|
||||
AUTH -->|"authorized"| REST["DWH REST"]
|
||||
```
|
||||
|
||||
Scegliere l'architettura corretta. Il template PSD usa il gruppo Nginx `www-data`; confermarlo
|
||||
prima dell'installazione su un host diverso.
|
||||
## Security boundaries
|
||||
|
||||
```bash
|
||||
sudo groupadd --system dwh-auth
|
||||
sudo useradd --system --no-create-home --shell /usr/sbin/nologin --gid dwh-auth dwh-auth
|
||||
sudo install -o root -g root -m 0755 /tmp/dwh-auth-release/dwh-auth-linux-amd64 /usr/local/sbin/dwh-auth
|
||||
sudo install -o root -g root -m 0644 deploy/dwh-auth/dwh-auth.service /etc/systemd/system/dwh-auth.service
|
||||
sudo install -o root -g root -m 0644 deploy/dwh-auth/dwh-auth.tmpfiles.conf /usr/lib/tmpfiles.d/dwh-auth.conf
|
||||
sudo install -d -o root -g root -m 0700 /root/dwh-auth-provision
|
||||
sudo systemd-tmpfiles --create /usr/lib/tmpfiles.d/dwh-auth.conf
|
||||
sudo /usr/local/sbin/dwh-auth --registry-root /var/lib/dwh-auth check
|
||||
sudo systemd-analyze verify /etc/systemd/system/dwh-auth.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now dwh-auth
|
||||
sudo systemctl status dwh-auth --no-pager
|
||||
```
|
||||
- A key identifies an installation, not a person.
|
||||
- Keys and backups stay in protected files and never enter Git, logs, arguments, or public JSON.
|
||||
- The registry stores digests and metadata, never the key in plaintext.
|
||||
- The REST route must be exposed only through verified TLS.
|
||||
|
||||
Controllare i mode con `stat`. Il servizio apre il registro in sola lettura e crea solo il socket.
|
||||
Non creare JSON, lock o socket a mano: oggetti insicuri devono fallire chiusi.
|
||||
## Installation
|
||||
|
||||
## Check, elenco e stato
|
||||
Il servizio usa questi percorsi:
|
||||
|
||||
Usare sempre un root assoluto. Questi comandi espongono solo ID pubblici, stato, date e scadenza:
|
||||
|
||||
```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
|
||||
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,
|
||||
non una correzione manuale del record.
|
||||
|
||||
## Creazione, consegna, scadenza e revoca
|
||||
|
||||
Il comando crea la chiave una volta in un nuovo file assoluto `0600`; stdout contiene solo ID
|
||||
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/psd-mac-primary.key
|
||||
sudo /usr/local/sbin/dwh-auth --registry-root /var/lib/dwh-auth key create \
|
||||
--installation-id "$installation_id" \
|
||||
--description "$description" \
|
||||
--output "$key_output"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
L'import legacy è temporaneo PSD: il file sorgente è già `root:root` `0600` e non viene mai letto o
|
||||
stampato dall'operatore.
|
||||
|
||||
```bash
|
||||
sudo /usr/local/sbin/dwh-auth --registry-root /var/lib/dwh-auth key import \
|
||||
--legacy-raw --installation-id legacy-shared \
|
||||
--from-file /root/dwh-auth-provision/legacy-shared.key
|
||||
```
|
||||
|
||||
Per rotare: creare seconda generazione, consegnarla, configurarla e provare `/rpc/ping`; confermare
|
||||
l'ID pubblico; attendere l'osservazione; poi revocare la precedente e provare nuova=successo,
|
||||
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_key_id" --reason "$revocation_reason"
|
||||
```
|
||||
|
||||
La revoca non è annullabile e un ID revocato non si ricrea.
|
||||
|
||||
## Backup, rollback e disinstallazione
|
||||
|
||||
Prima di mutare, creare un archivio root-only `0600` del registro e copie protette delle sole
|
||||
configurazioni coinvolte. L'archivio contiene digest, quindi è materiale riservato: custodirlo su
|
||||
storage cifrato approvato; l'evidenza ammessa riporta solo percorso, owner, mode, timestamp e
|
||||
checksum dell'archivio. Il rollback dual-key ripristina la route e il servizio revisionati, esegue
|
||||
`nginx -t` e fa reload solo autorizzato; non ripristina chiavi revocate, PostgreSQL, sessioni
|
||||
legacy, indici Qdrant o cache Ollama.
|
||||
|
||||
La disinstallazione richiede autorizzazione esplicita, client REST migrati/revocati e rollback non
|
||||
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 su storage cifrato approvato.
|
||||
|
||||
```bash
|
||||
run_id=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
backup_root=/root/dwh-auth-provision
|
||||
registry_root=/var/lib/dwh-auth
|
||||
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
|
||||
```
|
||||
|
||||
Il ripristino non sovrappone mai un tar al registro attivo. Estrarre prima in staging nello stesso
|
||||
filesystem di `/var/lib`, verificare il candidato, rinominare il registro attuale in una copia
|
||||
recuperabile e sostituirlo. Non cancellare il pre-ripristino: serve al rollback se `check` o
|
||||
l'avvio falliscono.
|
||||
|
||||
```bash
|
||||
registry_staging="/var/lib/.dwh-auth-restore-$run_id"
|
||||
registry_candidate="$registry_staging/dwh-auth"
|
||||
registry_previous="/var/lib/dwh-auth.pre-restore-$run_id"
|
||||
if [ -e "$registry_staging" ] || [ -e "$registry_previous" ]; then printf 'registry_restore=FAIL\n' >&2; exit 1; fi
|
||||
if ! sudo sha256sum -c "$manifest" >/dev/null; then printf 'registry_restore=FAIL\n' >&2; exit 1; fi
|
||||
if ! sudo install -d -o root -g root -m 0700 "$registry_staging"; then printf 'registry_restore=FAIL\n' >&2; exit 1; fi
|
||||
if ! sudo tar --acls --xattrs -C "$registry_staging" -xf "$registry_backup"; then printf 'registry_restore=FAIL\n' >&2; exit 1; fi
|
||||
if ! sudo /usr/local/sbin/dwh-auth --registry-root "$registry_candidate" check; then printf 'registry_restore=FAIL\n' >&2; exit 1; fi
|
||||
if ! sudo systemctl stop dwh-auth; then printf 'registry_restore=FAIL\n' >&2; exit 1; fi
|
||||
if ! sudo mv -T -- "$registry_root" "$registry_previous"; then
|
||||
if sudo systemctl start dwh-auth; then printf 'registry_restore_rollback=PASS\n' >&2; else printf 'registry_restore_rollback=FAIL\n' >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
if ! sudo mv -T -- "$registry_candidate" "$registry_root"; then
|
||||
if ! sudo mv -T -- "$registry_previous" "$registry_root"; then printf 'registry_restore_rollback=FAIL\n' >&2; exit 1; fi
|
||||
if sudo /usr/local/sbin/dwh-auth --registry-root "$registry_root" check && sudo systemctl start dwh-auth; then printf 'registry_restore_rollback=PASS\n' >&2; else printf 'registry_restore_rollback=FAIL\n' >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
if sudo /usr/local/sbin/dwh-auth --registry-root "$registry_root" check && sudo systemctl start dwh-auth; then
|
||||
printf 'registry_restore=PASS\n'
|
||||
else
|
||||
sudo systemctl stop dwh-auth || true
|
||||
if ! sudo mv -T -- "$registry_root" "$registry_staging/failed-dwh-auth"; then printf 'registry_restore_rollback=FAIL\n' >&2; exit 1; fi
|
||||
if ! sudo mv -T -- "$registry_previous" "$registry_root"; then printf 'registry_restore_rollback=FAIL\n' >&2; exit 1; fi
|
||||
if sudo /usr/local/sbin/dwh-auth --registry-root "$registry_root" check && sudo systemctl start dwh-auth; then printf 'registry_restore_rollback=PASS\n' >&2; else printf 'registry_restore_rollback=FAIL\n' >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
Per le prove, creare file header `0600` che contengono esattamente `X-API-Key: valore`. Il valore
|
||||
passa dal file chiave al file header senza argv, ambiente o stdout. I file header sono materiale
|
||||
segreto con la stessa custodia e retention delle chiavi.
|
||||
|
||||
```bash
|
||||
v1_key_file="$key_output"
|
||||
legacy_key_file=/root/dwh-auth-provision/legacy-shared.key
|
||||
v1_header_file=/root/dwh-auth-provision/dwh-auth-v1.header
|
||||
legacy_header_file=/root/dwh-auth-provision/dwh-auth-legacy.header
|
||||
random_header_file=/root/dwh-auth-provision/dwh-auth-random.header
|
||||
if ! sudo python3 -c '
|
||||
import pathlib, sys
|
||||
if any(b"\n" in pathlib.Path(path).read_bytes() for path in sys.argv[1:]):
|
||||
raise SystemExit(1)
|
||||
' "$v1_key_file" "$legacy_key_file"; then
|
||||
printf 'key_file_bytes=FAIL\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'key_file_bytes=PASS\n'
|
||||
for header_file in "$v1_header_file" "$legacy_header_file" "$random_header_file"; do
|
||||
sudo install -o root -g root -m 0600 /dev/null "$header_file"
|
||||
done
|
||||
sudo sh -c '{ printf "%s" "X-API-Key: "; dd if="$1" bs=65536 status=none; printf "\n"; } > "$2"' sh "$v1_key_file" "$v1_header_file"
|
||||
sudo sh -c '{ printf "%s" "X-API-Key: "; dd if="$1" bs=65536 status=none; printf "\n"; } > "$2"' sh "$legacy_key_file" "$legacy_header_file"
|
||||
sudo sh -c 'printf "%s\n" "X-API-Key: invalid-test" > "$1"' sh "$random_header_file"
|
||||
```
|
||||
|
||||
Il socket `/verify` deve restituire 204 per v1 e legacy durante il dual-key, 401 per file casuale
|
||||
e richiesta senza header. Stampare solo PASS/FAIL.
|
||||
|
||||
```bash
|
||||
status=$(sudo curl --header "@$v1_header_file" --unix-socket /run/dwh-auth/verify.sock --output /dev/null --silent --show-error --write-out '%{http_code}' http://localhost/verify)
|
||||
[ "$status" = 204 ] && printf 'socket_v1=PASS\n' || { printf 'socket_v1=FAIL\n' >&2; exit 1; }
|
||||
status=$(sudo curl --header "@$legacy_header_file" --unix-socket /run/dwh-auth/verify.sock --output /dev/null --silent --show-error --write-out '%{http_code}' http://localhost/verify)
|
||||
[ "$status" = 204 ] && printf 'socket_legacy=PASS\n' || { printf 'socket_legacy=FAIL\n' >&2; exit 1; }
|
||||
status=$(sudo curl --header "@$random_header_file" --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 i file header protetti e la CA approvata contro `/dwh/rpc/ping`: PostgREST
|
||||
può restituire qualsiasi 2xx, non si pretende 204. Prima della revoca, v1 e legacy devono dare
|
||||
2xx; il file casuale deve dare 401.
|
||||
|
||||
```bash
|
||||
ping_url=https://supabase-aritmolab.policlinicosandonato.it/dwh/rpc/ping
|
||||
ca_file=/root/dwh-auth-provision/psd-dwh-ca.pem
|
||||
status=$(sudo curl --header "@$v1_header_file" --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_v1_pre_revoke=PASS\n' ;; *) printf 'https_v1_pre_revoke=FAIL\n' >&2; exit 1 ;; esac
|
||||
status=$(sudo curl --header "@$legacy_header_file" --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_legacy_pre_revoke=PASS\n' ;; *) printf 'https_legacy_pre_revoke=FAIL\n' >&2; exit 1 ;; esac
|
||||
status=$(sudo curl --header "@$random_header_file" --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; }
|
||||
```
|
||||
|
||||
Dopo l'osservazione, revocare solo la legacy usando il suo ID pubblico già registrato. Dopo la
|
||||
revoca v1 resta 2xx e legacy diventa 401 anche via HTTPS.
|
||||
|
||||
```bash
|
||||
legacy_key_id=legacy-shared
|
||||
sudo /usr/local/sbin/dwh-auth --registry-root "$registry_root" key revoke --key-id "$legacy_key_id" --reason shared-credential-rotation
|
||||
status=$(sudo curl --header "@$v1_header_file" --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_v1_post_revoke=PASS\n' ;; *) printf 'https_v1_post_revoke=FAIL\n' >&2; exit 1 ;; esac
|
||||
status=$(sudo curl --header "@$legacy_header_file" --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_legacy_post_revoke=PASS\n' || { printf 'https_legacy_post_revoke=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 --header "@$random_header_file" --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: controlla davvero le chiavi v1 e legacy leggendo solo i
|
||||
percorsi dei file da argv, e conserva anche la difesa generica per prefisso e digest. Il filtro
|
||||
emette solo PASS/FAIL.
|
||||
|
||||
```bash
|
||||
since=$(date -u -d '15 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
|
||||
if sudo python3 -c '
|
||||
import pathlib, subprocess, sys
|
||||
max_journal_bytes = 1_048_576
|
||||
max_journal_lines = 10_000
|
||||
max_chunk_bytes = 65_536
|
||||
process = None
|
||||
try:
|
||||
actual_keys = {pathlib.Path(path).read_bytes() for path in sys.argv[2:]}
|
||||
needles = (b"thtdwh_v1", b"secret_sha256", *actual_keys)
|
||||
max_needle_length = max(map(len, needles))
|
||||
process = subprocess.Popen(
|
||||
["journalctl", "-u", "dwh-auth", "--since", sys.argv[1], "--no-pager", "--output=cat"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except OSError:
|
||||
raise SystemExit(2)
|
||||
def stop_child():
|
||||
if process is not None:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait()
|
||||
bytes_seen = 0
|
||||
line_count = 0
|
||||
line_open = False
|
||||
carry = b""
|
||||
try:
|
||||
while True:
|
||||
remaining = max_journal_bytes - bytes_seen
|
||||
if remaining == 0:
|
||||
if process.stdout.read1(1):
|
||||
raise SystemExit(2)
|
||||
break
|
||||
chunk = process.stdout.read1(min(max_chunk_bytes, remaining))
|
||||
if not chunk:
|
||||
break
|
||||
bytes_seen += len(chunk)
|
||||
searchable = carry + chunk
|
||||
if any(needle in searchable for needle in needles):
|
||||
raise SystemExit(1)
|
||||
carry = searchable[-(max_needle_length - 1):]
|
||||
for byte in chunk:
|
||||
if byte == 10:
|
||||
line_count += 1
|
||||
line_open = False
|
||||
if line_count > max_journal_lines:
|
||||
raise SystemExit(2)
|
||||
else:
|
||||
line_open = True
|
||||
if line_open:
|
||||
line_count += 1
|
||||
if line_count > max_journal_lines:
|
||||
raise SystemExit(2)
|
||||
finally:
|
||||
stop_child()
|
||||
if process.returncode != 0:
|
||||
raise SystemExit(2)
|
||||
' "$since" "$v1_key_file" "$legacy_key_file"; then
|
||||
printf 'journal_actual_key_scan=PASS\n'
|
||||
else
|
||||
printf 'journal_actual_key_scan=FAIL\n' >&2
|
||||
exit 1
|
||||
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, manifest e file header protetti per la retention; non cancellarli durante il
|
||||
rollback.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Sintomo | Interpretazione e azione |
|
||||
| Oggetto | Percorso |
|
||||
| --- | --- |
|
||||
| `401` | Chiave assente, malformata, sconosciuta, scaduta, revocata o errata. Verificare trasporto, ID pubblico e consegna; non cercare dettagli nel messaggio. |
|
||||
| `503` | Servizio, socket o registro non disponibile/sicuro. Controllare `systemctl`, socket, mode e `check`; ripristinare il backup approvato. |
|
||||
| `check` fallisce | Integrità del registro non valida. Fermare le scritture, preservare stato e ripristinare; non editare JSON. |
|
||||
| TLS fallisce | CA o SAN non validi. Seguire [TLS](dwh-auth-tls.md), senza bypass. |
|
||||
| Binario | `/usr/local/sbin/dwh-auth` |
|
||||
| Unit systemd | `/etc/systemd/system/dwh-auth.service` |
|
||||
| Registro | `/var/lib/dwh-auth/` |
|
||||
| Socket | `/run/dwh-auth/verify.sock` |
|
||||
| Consegne protette | `/root/dwh-auth-provision/` |
|
||||
|
||||
Per il rollout PSD con i due gate separati vedere il [runbook PSD](../operations/psd-dwh-auth-rollout.md).
|
||||
Install the binary and unit with `root` ownership, create the `dwh-auth` service user, and enable
|
||||
the unit with `systemctl enable --now dwh-auth`. The socket must be accessible to Nginx's group.
|
||||
|
||||
## Creating and revoking keys
|
||||
|
||||
Esempio per l'installazione ACME Limited:
|
||||
|
||||
```bash
|
||||
sudo dwh-auth --registry-root /var/lib/dwh-auth key create \
|
||||
--installation-id acme-factory-primary \
|
||||
--description acme-factory-primary \
|
||||
--output /root/dwh-auth-provision/acme-factory-primary.key
|
||||
```
|
||||
|
||||
Deliver the file through an enterprise vault or an authenticated channel. To rotate a key, create
|
||||
a new one, distribute it, update the client, and revoke the old one using its public ID:
|
||||
|
||||
```bash
|
||||
sudo dwh-auth --registry-root /var/lib/dwh-auth key revoke \
|
||||
--key-id PUBLIC_KEY_ID \
|
||||
--reason scheduled-rotation
|
||||
```
|
||||
|
||||
Revocation is permanent. Keep encrypted registry backups before every mutation.
|
||||
|
||||
## Nginx integration
|
||||
|
||||
Nginx forwards the key to the `dwh-auth` socket. Only an authorized response allows the request
|
||||
to reach DWH REST. Missing, unknown, expired, or revoked keys receive `401`; an unavailable
|
||||
service or registry produces `503`.
|
||||
|
||||
@@ -1,49 +1,38 @@
|
||||
# TLS per DWH REST
|
||||
# TLS for DWH REST
|
||||
|
||||
La chiave DWH è accettabile solo sopra TLS verificato. Un errore `401` o `503` non autorizza mai a
|
||||
ridurre la verifica del certificato.
|
||||
The DWH key may be used only over verified TLS. Authorization or availability errors never justify
|
||||
disabling certificate verification.
|
||||
|
||||
## Stato PSD
|
||||
## Private CA
|
||||
|
||||
L'origine REST PSD corrente usa il certificato self-issued/private di Nginx. Il SAN copre
|
||||
`supabase-aritmolab.policlinicosandonato.it`, l'origine `.it` approvata, e non copre un dominio
|
||||
`.com`. Non usare `.com` finché non è incluso esplicitamente nel SAN.
|
||||
When DWH REST uses an enterprise CA, deliver the certificate separately from the API key. The CA
|
||||
is not a credential, but its integrity is part of the security boundary. Keep it out of Git and
|
||||
make it unwritable by unauthorized users.
|
||||
|
||||
Chi non dispone già di trust equivalente approvato riceve la CA separatamente e configura
|
||||
`TLS_CA_FILE`. La CA non è una credenziale, ma la sua integrità è un confine di sicurezza: fuori da
|
||||
Git e non scrivibile da utenti non autorizzati.
|
||||
|
||||
## Fingerprint fuori banda
|
||||
|
||||
Calcolare localmente il fingerprint del file ricevuto:
|
||||
|
||||
```bash
|
||||
openssl x509 -noout -fingerprint -sha256 -in /absolute/protected/psd-dwh-ca.pem
|
||||
```
|
||||
|
||||
Confrontarlo con il responsabile autorizzato tramite un canale indipendente dalla consegna (vault
|
||||
aziendale o canale telefonico verificato). Nell'evidenza registrare solo conferma, approvatore e
|
||||
timestamp; mai corpo certificato, fingerprint completo o output grezzo.
|
||||
|
||||
## Binding e ping
|
||||
|
||||
Il binding headless PSD effettivo è:
|
||||
Esempio ACME Limited:
|
||||
|
||||
```dotenv
|
||||
THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE=/run/secrets/psd-clinical-dwh-ca.pem
|
||||
THT_WS_ACME_EBIKES_DWH_TLS_CA_FILE=/run/secrets/acme-ebikes-dwh-ca.pem
|
||||
```
|
||||
|
||||
Il file sorgente locale è collegato da file operatore non tracciato. Usare URL `.it`, poi
|
||||
**Test workspace connections** su `/rpc/ping`. Non disabilitare TLS e non usare `curl -k`.
|
||||
## Out-of-band fingerprint
|
||||
|
||||
## Rinnovo coordinato
|
||||
Calculate the fingerprint of the received file and compare it through an independent channel:
|
||||
|
||||
1. Preparare certificato e chain nuovi; verificare prima SAN `.it` e assenza di falsa copertura `.com`.
|
||||
2. Confermare fuori banda il nuovo fingerprint.
|
||||
3. Consegnare la CA/chain nuova ai client con `TLS_CA_FILE`, senza rimuovere ancora la precedente.
|
||||
4. Aggiornare vault/binding e verificare ping con TLS normale.
|
||||
5. Solo con gate Nginx approvato installare il certificato server e ripetere il ping.
|
||||
6. Ritirare il trust precedente dopo la finestra approvata.
|
||||
```bash
|
||||
openssl x509 -noout -fingerprint -sha256 \
|
||||
-in /absolute/protected/acme-ebikes-dwh-ca.pem
|
||||
```
|
||||
|
||||
Il rinnovo non modifica chiavi `dwh-auth`, record o ruoli PostgreSQL. TLS e rollback della route
|
||||
restano approvazioni e backup distinti.
|
||||
The certificate SAN must include the exact name used by the binding, such as `dwh.acme.example`.
|
||||
|
||||
## Renewal
|
||||
|
||||
1. Prepare the new certificate and chain.
|
||||
2. Confirm the SAN and fingerprint out of band.
|
||||
3. Distribute the new CA to clients while temporarily keeping the old one.
|
||||
4. Update the binding and confirm connectivity with normal TLS.
|
||||
5. Install the server certificate.
|
||||
6. Remove the old trust after the agreed window.
|
||||
|
||||
Do not use `curl -k`, disable TLS, or embed complete certificates or fingerprints in shared documents.
|
||||
|
||||
Reference in New Issue
Block a user