Harden the container: non-root, pinned images and dependencies, the key in a file (B-91, SEC-51, SEC-52, SEC-53; closes B-71, B-72)

- app runs as uid 1000; a one-shot data-permissions service chowns ./data to it
  and closes it to others (works on an older root-owned install with no host sudo);
  files are created 0600 (umask 077)
- python:3.12-slim and caddy:2-alpine pinned by digest; dependencies installed from
  requirements.lock with --require-hashes
- the Fernet key is XPRV_ENCRYPTION_KEY_FILE (./secrets/xprv_encryption.key, a compose
  secret), out of every environment variable and of a ./data backup
- setup.md no longer presents a second way to run the server (B-72)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-18 00:54:23 +02:00
1 parent 40ac490826
commit c1b192f6d1
23 files changed
+1500 -183

No files matched your search

@@ -1,6 +1,6 @@
# ADR-01 — La piattaforma è custodiale: tutte le chiavi sono generate e tenute dal server
- **Stato:** Accettata — implementazione da completare (B-91)
- **Stato:** Accettata
- **Area:** Custodia, chiavi e indirizzi
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
@@ -23,5 +23,3 @@ git. L'utente non controlla nulla finché non preleva.
*Codice:* `app/wallet/hd.py:_load_account_key` · `app/wallet/keystore.py:decrypt_xprv` · `app/wallet/hd.py:generate_master_key` ·
*Test:* `tests/unit/test_hd.py`
*Stato:* da implementare (B-91) · *Oggi:* la chiave di cifratura è la
variabile d'ambiente `XPRV_ENCRYPTION_KEY`, visibile con `docker inspect`.
@@ -1,6 +1,6 @@
# ADR-11 — Il server rifiuta di partire con segreti inadeguati
- **Stato:** Accettata — implementazione da completare (B-91)
- **Stato:** Accettata
- **Area:** Configurazione e ambiente
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
@@ -24,8 +24,6 @@ chiuso a tutti. Il controllo è nel lifespan, non in un validatore di
*Codice:* `app/config.py:validate_runtime_secrets` · `app/main.py:lifespan` · `app/api/routes/admin.py:require_admin` ·
*Test:* `tests/unit/test_config.py`
*Stato:* da implementare (B-91) per la parte sulla chiave · *Oggi:* la
chiave Fernet arriva da `XPRV_ENCRYPTION_KEY` (variabile d'ambiente); dopo
B-91 arriva da un file (`XPRV_ENCRYPTION_KEY_FILE`,
La chiave Fernet arriva da un file (`XPRV_ENCRYPTION_KEY_FILE`,
[sicurezza.md](../sicurezza.md#sec-53) SEC-53) e il rifiuto riguarda un file
mancante o vuoto.
+6 -10
View File
@@ -55,9 +55,8 @@ flowchart LR
installate da un file di lock con gli hash
([sicurezza.md](sicurezza.md#sec-51) SEC-51, SEC-52, SEC-53 (B-91)). I controlli che richiedono il codice
applicativo (chiave, quorum, ripristino) sono nel lifespan ([ARC-60]).
*Codice:* `Dockerfile` (`CMD`) · `migrations/env.py`
*Stato:* da implementare (B-91) · *Oggi:* il container gira come root,
`python:3.12-slim` non è fissato per digest e le dipendenze sono `>=`.
*Codice:* `Dockerfile` (`CMD`) · `migrations/env.py` · `requirements.lock` ·
*Test:* `tests/unit/test_container_hardening.py`
- **[ARC-04]** `app` è solo `expose`d sulla rete interna di compose (porta
8123), mai pubblicato sull'host: l'unico ingresso è Caddy, che pubblica 80 e
443. Su questo si basa la lettura dell'IP del client (vedi
@@ -79,9 +78,9 @@ flowchart LR
rilascio, così un rollback sa quale immagine rimettere
([operativita.md](operativita.md) (B-93)).
*Codice:* `docker-compose.yml`
*Stato:* da implementare (B-91, B-93) · *Oggi:* la chiave di cifratura è la
variabile `XPRV_ENCRYPTION_KEY` in `.env`, i permessi sono quelli di default
(`.env` e file di chiave leggibili da tutti) e l'immagine non ha etichetta.
*Test:* `tests/unit/test_container_hardening.py`
*Stato:* da implementare (B-93), per l'etichetta per rilascio · *Oggi:*
l'immagine di `app` ha l'etichetta fissa `plm-lottery-app:local`.
- **[ARC-61]** **Salute del container.** Il container `app` ha un
`healthcheck` che interroga `http://127.0.0.1:8123/health/ready` ogni 30s
@@ -415,8 +414,6 @@ implementare» non esistono ancora nel codice (vedi [ARC-60] e [ARC-22]).
veri.
*Codice:* `app/config.py:validate_runtime_secrets` ·
*Test:* `tests/unit/test_config.py`
*Stato:* da implementare (B-91) · *Oggi:* si controlla che la variabile
`XPRV_ENCRYPTION_KEY` non sia vuota.
- **[ARC-20]** Gli endpoint Electrum vengono letti da `ELECTRUM_HOST`,
`ELECTRUM_PORT`, `ELECTRUM_USE_SSL` ed `ELECTRUM_FALLBACK_SERVERS`
(`host:porta[:notls][:pin=<sha256>]` separati da virgola, [ARC-30]); un
@@ -598,8 +595,7 @@ La configurazione è divisa in due posti con regole diverse.
| `enable_api_docs` | `ENABLE_API_DOCS` | `False` | Espone Swagger/ReDoc/OpenAPI ([ARC-14]) |
*Codice:* `app/config.py:Settings` · *Test:* `tests/unit/test_config.py`
*Stato:* da implementare (B-91), per `XPRV_ENCRYPTION_KEY_FILE` · *Oggi:* la
chiave Fernet è la variabile `XPRV_ENCRYPTION_KEY`.
- **[ARC-31]** **Parametri operativi e di business** (costo della scommessa,
durata e pausa del round, animazione, commissione di rete, timeout RBF,
indirizzo commissioni, pausa di manutenzione e suo motivo, conferme dei
+1 -1
View File
@@ -581,7 +581,7 @@ restano nella tabella come storico.
`secrets.token_bytes(32)`. All'inserimento del round si scrivono insieme
`server_seed_hash` (`sha256` di quei 32 byte, esadecimale minuscolo) e
`server_seed_enc`: il segreto **cifrato con Fernet** usando la stessa chiave
che cifra la master xprv (`XPRV_ENCRYPTION_KEY`, dopo B-91 il file di
che cifra la master xprv (il file di cifratura,
[sicurezza.md](sicurezza.md#sec-53) SEC-53). Il segreto in chiaro non viene
mai scritto nel database prima della rivelazione: `server_seed` resta
`NULL` e si valorizza (esadecimale minuscolo, 64 caratteri) nella stessa
+5 -5
View File
@@ -44,9 +44,9 @@ flowchart LR
Docker con nome `caddy_data` e `caddy_config`, non in `./data`.
*Codice:* `docker-compose.yml`, `Caddyfile`
Oggi il container gira come root, e i file che scrive in `./data` (database,
backup, chiave cifrata dagli script) appartengono a root sull'host. Dopo B-91
gira come utente non root, con `./data` in `0700` e i file di chiave in
Il container gira come utente non root (uid 1000, quello dell'utente dell'host:
un servizio `data-permissions`, a ogni avvio, riporta `./data` a quel
proprietario), con `./data` in `0700` e i file di chiave in
`0600`, e la chiave di cifratura sta in un file a parte
([architettura.md](architettura.md) ARC-03, ARC-05;
[SEC-51](sicurezza.md#sec-51)); le procedure che scrivono in `./data`
@@ -106,7 +106,7 @@ Il file da salvare è `./data/keys/master.xprv.enc`, quello che il container
legge. Su questa installazione esiste anche una seconda copia,
`./master.xprv.enc` nella radice del progetto, a cui punta il
`MASTER_KEY_PATH` di `.env` (B-71): oggi decifrano alla stessa xprv, ma non è
il file dell'app. Dopo B-91 la chiave di cifratura non sta più in `.env` ma
il file dell'app. La chiave di cifratura non sta più in `.env` ma
nel file `./secrets/xprv_encryption.key` ([SEC-53](sicurezza.md#sec-53),
[architettura.md](architettura.md) ARC-05), fuori da `./data/`: si salva come
oggi la variabile, separata dalla master key.
@@ -750,7 +750,7 @@ con `docker compose logs app` ([architettura.md](architettura.md) ARC-09).
root con permessi `0644`: `sudo chmod 600 data/keys/master.xprv.enc`.
Gli script scrivono `0600` e rifiutano una xprv i cui
indirizzi non coincidono con quelli del database
([SEC-50](sicurezza.md#sec-50)); dopo B-91 la chiave sta in un file e non
([SEC-50](sicurezza.md#sec-50)); la chiave sta in un file e non
in `.env` ([SEC-53](sicurezza.md#sec-53)).
**Se è trapelata la master key stessa** (file cifrato *e* chiave, oppure la
+16 -18
View File
@@ -1,13 +1,13 @@
# Setup
Passaggi da eseguire una tantum per preparare un'istanza di PLM Lottery, prima di
poterla avviare (in locale o via Docker). Per come avviarla poi ogni volta, vedi
poterla avviare. Il server gira sempre in Docker; il venv serve solo a test, migrazioni da generare e script una tantum. Per come avviarla poi ogni volta, vedi
[running-the-server.md](running-the-server.md).
## 1. Prerequisiti
- Python 3.12+ (serve solo per il workflow locale/venv — puoi saltarlo se usi solo Docker)
- Docker + Docker Compose (serve solo per il workflow a container)
- Docker + Docker Compose (il server gira solo così)
- Python 3.12+ (solo per gli strumenti locali: test, `alembic revision` e gli script delle chiavi)
- Un server Electrum raggiungibile per la rete PLM. Il server di bootstrap per lo
sviluppo è `santantonio.sytes.net:50002` (SSL) — va bene per i test, ma in
produzione conviene usarne uno di cui ci si fida o gestirne uno proprio.
@@ -31,7 +31,7 @@ cp .env.example .env
| Variabile | Scopo | Come generarla |
|---|---|---|
| `XPRV_ENCRYPTION_KEY` | Chiave simmetrica che cifra a riposo la master xprv del server. **Perdere questa chiave significa perdere per sempre l'accesso ai fondi di tutti gli utenti.** | `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` |
| `XPRV_ENCRYPTION_KEY_FILE` | Percorso del file che contiene la chiave simmetrica che cifra a riposo la master xprv del server (`./secrets/xprv_encryption.key`, cartella `0700`, file `0600`; nel container è montato come secret). **Perdere questa chiave significa perdere per sempre l'accesso ai fondi di tutti gli utenti.** | `mkdir -p secrets && chmod 700 secrets && python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" > secrets/xprv_encryption.key && chmod 600 secrets/xprv_encryption.key` |
| `JWT_SECRET` | Firma i token di sessione degli utenti. | `python -c "import secrets; print(secrets.token_urlsafe(32))"` |
| `ADMIN_TOKEN` | Token bearer richiesto sugli endpoint admin (header `X-Admin-Token`). | `python -c "import secrets; print(secrets.token_urlsafe(32))"` |
@@ -61,7 +61,7 @@ certificato del primario. Attenzione: un valore scritto male **blocca
l'avvio** dell'app — è voluto, meglio accorgersene subito che durante il
disservizio in cui il fallback serve davvero.
`JWT_SECRET` e `XPRV_ENCRYPTION_KEY` vengono verificati all'avvio: se sono vuoti
`JWT_SECRET` e la chiave in `XPRV_ENCRYPTION_KEY_FILE` vengono verificati all'avvio: se sono vuoti
(o `JWT_SECRET` è più corto di 32 caratteri) il container si rifiuta di partire con
un errore esplicito, invece di avviarsi e rompersi al primo login. Nota: `.env`
contiene solo segreti e configurazione di infrastruttura — i parametri di
@@ -74,15 +74,13 @@ admin dopo l'avvio, non qui — vedi [guida-admin.md](guida-admin.md).
Il server deriva l'indirizzo di deposito di ogni utente (e l'indirizzo pool) da
un'unica master xprv, generata una volta e cifrata a riposo con
`XPRV_ENCRYPTION_KEY`. Questo passaggio va eseguito esattamente una volta per
ogni deployment, dopo aver impostato `XPRV_ENCRYPTION_KEY` in `.env`:
la chiave in `XPRV_ENCRYPTION_KEY_FILE`. Questo passaggio va eseguito esattamente una volta per
ogni deployment, dopo aver creato quel file e impostato `XPRV_ENCRYPTION_KEY_FILE` in `.env`:
- **Locale/venv**: `PYTHONPATH=. python scripts/generate_master_key.py`
- **Docker**: `docker compose run --rm app python scripts/generate_master_key.py`
Dal venv: `PYTHONPATH=. python scripts/generate_master_key.py`, con `MASTER_KEY_PATH=./data/keys/master.xprv.enc` in `.env`, così lo script scrive esattamente il file che legge il container.
Questo scrive un file cifrato (`MASTER_KEY_PATH`, default `./master.xprv.enc` in
locale o `./data/keys/master.xprv.enc` con Docker). **Fai il backup di questo
file insieme a `XPRV_ENCRYPTION_KEY`** — uno dei due da solo è inutile, ma
Questo scrive un file cifrato (`MASTER_KEY_PATH`, in Docker `./data/keys/master.xprv.enc`). **Fai il backup di questo
file insieme a `secrets/xprv_encryption.key`** — uno dei due da solo è inutile, ma
perderli entrambi insieme significa perdere i fondi di tutti gli utenti senza
possibilità di recupero.
@@ -107,10 +105,10 @@ Entrambi vanno eseguiti localmente (o dentro il container via
pannello admin: chi ottiene questa xprv ottiene il controllo dei fondi di
tutti gli utenti e del pool.
## 4. Installare le dipendenze (solo workflow locale/venv)
## 4. Installare gli strumenti locali (venv)
Salta questo passaggio se usi solo Docker — l'immagine installa le proprie
dipendenze durante la build.
Servono per i test e per gli script delle chiavi; il server non ne ha bisogno,
perché l'immagine installa le proprie dipendenze (da `requirements.lock`).
```bash
python3 -m venv .venv
@@ -120,9 +118,9 @@ pip install -e ".[dev]"
## 5. Applicare le migrazioni del database
- **Locale/venv**: `alembic upgrade head`
- **Docker**: le migrazioni vengono eseguite automaticamente all'avvio del
container (vedi il `CMD` del `Dockerfile`) — nessun passaggio manuale.
Le migrazioni vengono eseguite automaticamente all'avvio del container (vedi il
`CMD` del `Dockerfile`) — nessun passaggio manuale, e `alembic upgrade head` non
si lancia mai a mano.
## 6. Impostare l'indirizzo delle fee
+4 -26
View File
@@ -104,11 +104,7 @@ Restano **accettati**, e descritti in
| Hash delle password | Tabella `users` (SQLite, `./data/db/`) | Argon2id | Attacco offline a dizionario, rallentato dai parametri Argon2 | — | Reset dall'operatore |
| JWT dell'utente | Browser, `localStorage` (`plm_token`) | Emesso al login/registrazione | Sessione dell'utente fino alla scadenza (24h) o al cambio password | — | Cambio password o reset dall'operatore |
*Oggi* (B-91): la chiave di cifratura è la variabile d'ambiente
`XPRV_ENCRYPTION_KEY` in `.env`, visibile a chiunque esegua `docker inspect`
sul container; `.env` e i file di chiave hanno i permessi predefiniti del
sistema (di norma `0644`, leggibili da tutti).
`JWT_SECRET` e `ADMIN_TOKEN` restano variabili d'ambiente anche dopo B-91: le
`JWT_SECRET` e `ADMIN_TOKEN` restano variabili d'ambiente (B-91): le
vede con `docker inspect` solo chi ha accesso a Docker, che è già fidato come
l'host.
@@ -118,10 +114,6 @@ l'host.
`master.xprv.enc`, i database `*.db`, `data/` e `logs/` sono esclusi sia da
git sia dall'immagine Docker.
*Codice:* `app/config.py:Settings`, `.gitignore`, `.dockerignore`
*Stato:* da implementare (B-91), per la chiave in un file e l'esclusione di
`secrets/` · *Oggi:* anche la chiave di cifratura sta in `.env`
(`XPRV_ENCRYPTION_KEY`), e né `.gitignore` né `.dockerignore` escludono
`secrets/`.
- **[SEC-03]** Tutti i parametri operativi stanno nel database
(`round_config`); in `.env` restano solo segreti, collegamenti
@@ -131,7 +123,6 @@ l'host.
riavvio. L'elenco completo è in
[architettura.md](architettura.md#configurazione).
*Codice:* `app/config.py:Settings`
*Stato:* da implementare (B-91), per `XPRV_ENCRYPTION_KEY_FILE`.
`.env.example` documenta le chiavi necessarie e il comando per generarle;
non elenca `DATABASE_URL` né `MASTER_KEY_PATH`, che in Docker sono comunque
@@ -204,10 +195,7 @@ sì che un backup dei dati non contenga entrambe.
container, quindi `docker inspect` non la mostra, e non finisce in un
backup di `./data/`.
*Codice:* `app/config.py:Settings`, `app/wallet/keystore.py:decrypt_xprv`,
`docker-compose.yml` · *Test:* —
*Stato:* da implementare (B-91) · *Oggi:* la chiave è la variabile
`XPRV_ENCRYPTION_KEY` di `.env`, passata al container con `env_file` e
visibile con `docker inspect`.
`docker-compose.yml` · *Test:* `tests/unit/test_container_hardening.py`, `tests/unit/test_config.py`
### Derivazione
@@ -265,8 +253,6 @@ riserva è [ELC-19](connessione-electrum.md#1-rotazione-tra-server) (vedi
reali.
*Codice:* `app/config.py:validate_runtime_secrets`, `app/main.py:lifespan` ·
*Test:* `tests/unit/test_config.py`
*Stato:* da implementare (B-91), per la chiave letta da file · *Oggi:* si
controlla che la variabile `XPRV_ENCRYPTION_KEY` non sia vuota.
- **[SEC-13]** Un `ADMIN_TOKEN` vuoto **non** blocca l'avvio: il pannello
risulta semplicemente chiuso (vedi [SEC-24]).
@@ -628,11 +614,7 @@ entra nell'immagine.
`0700`; `.env`, `master.xprv.enc` e `./secrets/xprv_encryption.key` hanno
`0600`; tutti appartengono all'utente con cui gira il container. I file che
l'app crea (database, backup, log) nascono con permessi `0600`.
*Codice:* `Dockerfile`, `docker-compose.yml` · *Test:* —
*Stato:* da implementare (B-91) · *Oggi:* il container gira come root, i
file che scrive in `./data/` appartengono a root sull'host, e `.env` e i
file di chiave hanno i permessi predefiniti del sistema (di norma `0644`,
leggibili da ogni utente dell'host).
*Codice:* `Dockerfile`, `docker-compose.yml` · *Test:* `tests/unit/test_container_hardening.py`
- **[SEC-52]** Ciò che entra nelle immagini è fissato: le immagini di base
(`python:3.12-slim` per `app`, `caddy:2-alpine` per `caddy`) sono indicate
@@ -640,11 +622,7 @@ entra nell'immagine.
di lock con l'hash di ogni pacchetto (`pip install --require-hashes`), non
dagli intervalli `>=` di `pyproject.toml`. Aggiornare una dipendenza o
un'immagine significa rigenerare il lock o cambiare il digest in un commit.
*Codice:* `Dockerfile`, `docker-compose.yml`, `pyproject.toml` · *Test:* —
*Stato:* da implementare (B-91) · *Oggi:* le immagini di base sono
indicate per etichetta mobile e le dipendenze con `>=`: ogni build può
installare versioni diverse, comprese versioni compromesse pubblicate dopo
l'ultima build.
*Codice:* `Dockerfile`, `docker-compose.yml`, `requirements.lock` · *Test:* `tests/unit/test_container_hardening.py`
La chiave di cifratura montata come file, e non come variabile d'ambiente, è
[SEC-53]. Le procedure che scrivono in `./data/` devono rispettarne il
+7 -8
View File
@@ -193,8 +193,8 @@ limiti accettati: lo stato `drawing` non riprende dopo un riavvio (vedi
| B-68 | CLAUDE.md e README descrivevano uno stato che il codice aveva superato | Bassa | `162ceed` Describe the code the docs actually ship with | `test_docs_current.py` | ARC |
| B-69 | Commenti nel codice non più veri | Bassa | `666cb1a` Retire in-code comments that outlived what they described | `test_code_comments.py` | ARC |
| B-70 | Il diagramma diceva che prelievo e scommessa in corso non potevano coesistere, ma il codice non lo impone (e non deve) | Bassa | `526a649` Say what bets and withdrawals actually exclude | `test_withdrawals.py::test_withdrawal_is_allowed_while_a_bet_is_still_unconfirmed`, `::test_the_docs_no_longer_claim_bets_and_withdrawals_are_exclusive` | WDR |
| B-71 | `.env` punta `MASTER_KEY_PATH` a una seconda copia della chiave master (e `DATABASE_URL` a un secondo database) invece che ai file in `./data/` letti dal container | Bassa | **aperto** | — | OPS, SEC |
| B-72 | `docs/setup.md` presenta ancora l'installazione come «in locale o via Docker», contro la regola «solo Docker» | Bassa | **aperto** | — | OPS |
| B-71 | `.env` punta `MASTER_KEY_PATH` a una seconda copia della chiave master (e `DATABASE_URL` a un secondo database) invece che ai file in `./data/` letti dal container | Bassa | corretto con l'allineamento dei file e B-89 | `test_master_key_check.py` | OPS, SEC |
| B-72 | `docs/setup.md` presenta ancora l'installazione come «in locale o via Docker», contro la regola «solo Docker» | Bassa | *da registrare* | — (documentazione) | OPS |
Note:
@@ -239,7 +239,7 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
| B-88 | Le transazioni SQLite non sono quelle che il codice presuppone; `synchronous=NORMAL` può perdere gli ultimi commit | Alta | *da registrare* | `test_sqlite_transactions.py`, `test_db_base.py` | DAT, ARC |
| B-89 | Problemi della chiave rilevati solo al primo uso; gli script accettano qualunque xprv e scrivono file leggibili da tutti | Alta | *da registrare* | `test_master_key_check.py` | SEC, OPS |
| B-90 | Il ripristino di un backup riapre round già pagati e riassegna indici di derivazione | Alta | *da registrare* | `test_restore_gap.py` | OPS, REG |
| B-91 | Container eseguito come root, permessi larghi, dipendenze e immagini non fissate | Alta | **aperto** | — | SEC, OPS |
| B-91 | Container eseguito come root, permessi larghi, dipendenze e immagini non fissate | Alta | *da registrare* | `test_container_hardening.py`, `test_config.py` | SEC, OPS |
| B-92 | Nessuna verifica di salute o prontezza, stato Electrum invisibile, audit non filtrabile, log incompleti | Media | **aperto** | — | OPS, AUD, API |
| B-93 | Deploy senza arresto ordinato, rollback impossibile, `SITE_ADDRESS` che torna a `localhost` | Media | **aperto** | — | OPS |
| B-94 | Cambio password, login e bug report senza limiti adeguati; errori non strutturati in alcune risposte | Media | **aperto** | — | SEC, API |
@@ -252,11 +252,10 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
## Stato attuale
- **Corretti:** B-01 … B-70, tranne B-16, che è chiuso nella sostanza (il
- **Corretti:** B-01 … B-72, tranne B-16, che è chiuso nella sostanza (il
crash è eliminato e `/guida` serve un segnaposto) ma lascia la guida vera
come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80, B-84, B-76, B-77, B-78, B-83, B-74, B-85, B-86, B-73, B-81, B-79, B-82, B-87, B-89, B-90, B-95, B-98, B-99, B-100.
- **Aperti:** B-71, B-72 (gravità bassa, operativi o di documentazione) e
B-73 … B-98 dalla revisione del 2026-09-28 tranne i corretti (0 critici,
3 alti, 5 medi), da implementare nell'ordine di
come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80, B-84, B-76, B-77, B-78, B-83, B-74, B-85, B-86, B-73, B-81, B-79, B-82, B-87, B-89, B-90, B-91, B-95, B-98, B-99, B-100.
- **Aperti:** dalla revisione del 2026-09-28, B-92, B-93, B-94 e B-96 (0 critici,
0 alti, 4 medi), da implementare nell'ordine di
[piano-correzioni.md](piano-correzioni.md).
- **Prossimo numero libero:** `B-101`.