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

+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