La garde de scripts/coffre-luks.sh refuse un conteneur LXC ou l'absence de device-mapper. L'ADR 0020, 10-infra.md et la vue d'ensemble disent ce qui est en place (SSE-C des archives) et ce qui relève de l'hôte Proxmox (chiffrement du disque du conteneur).
110 lines
8.4 KiB
Markdown
110 lines
8.4 KiB
Markdown
# 0020 - Chiffrement au repos : coffre LUKS des volumes Docker et SSE-C des archives
|
|
|
|
- Statut : accepté
|
|
- Date : 2026-09-24
|
|
|
|
## Contexte
|
|
|
|
L'issue #42 demande que les données de la plateforme soient chiffrées au repos. Tout ce que la
|
|
plateforme persiste vit dans les volumes Docker nommés des trois projets Compose de la VM ENI
|
|
([ADR 0009](0009-deux-environnements-compose-sur-la-vm-eni.md),
|
|
[ADR 0017](0017-environnement-dev-a-la-demande.md)), sous `/var/lib/docker/volumes` : la base
|
|
TimescaleDB (`pgdata`, relevés, comptes, audit), les métadonnées et les objets de Garage
|
|
(`garage_meta`, `garage_data`, les archives des chunks de `reading` exportées par le DAG
|
|
`retention`, [ADR 0019](0019-stockage-objet-garage-et-cycle-de-vie-des-mesures.md)), les journaux et l'état ML d'Airflow, les séries de Prometheus et la base
|
|
de Grafana.
|
|
|
|
Aucun des deux dépôts de données ne chiffre lui-même : Garage n'a pas de chiffrement côté serveur
|
|
et sa documentation renvoie à un volume LUKS sous ses données ; PostgreSQL communautaire n'a pas de
|
|
chiffrement transparent des données (TDE), et l'image `timescaledb-ha` n'en ajoute pas. La VM est
|
|
unique, sur un seul disque virtuel, sans partition libre, sans TPM, et personne n'est devant sa
|
|
console au démarrage : tout redémarrage doit aboutir sans saisie.
|
|
|
|
## Décision
|
|
|
|
**Constat du 24/09, qui borne tout ce qui suit.** La machine `eadl-2025-nantes-g3` n'est pas une
|
|
machine virtuelle mais un conteneur LXC Ubuntu 24.04 sur un hôte Proxmox (`systemd-detect-virt`
|
|
répond `lxc`, aucun `/dev/mapper/control`, aucun périphérique loop, pas de `/dev/fuse`, module
|
|
`dm_crypt` inaccessible). LUKS, comme tout chiffrement au niveau bloc ou FUSE, y est impossible.
|
|
Le chiffrement au repos du disque de ce conteneur ne peut se faire que sur l'hôte Proxmox
|
|
(volume LUKS ou ZFS chiffré sous le conteneur), par l'administrateur de l'école : la demande
|
|
lui est adressée, et jusqu'à sa réponse la base et les métadonnées Garage sont en clair sur ce
|
|
disque. `scripts/coffre-luks.sh` détecte ce cas et refuse de démarrer. Ce qui suit reste la
|
|
décision pour toute machine où le device-mapper est disponible (la cible k3s de `10-infra.md`,
|
|
ou une vraie VM), et le SSE-C des archives est en place dès aujourd'hui.
|
|
|
|
**Un coffre LUKS2 sous tous les volumes Docker, posé par `scripts/coffre-luks.sh`.**
|
|
|
|
- Le coffre est un **fichier image creux** (`/srv/enervision/coffre.img`, 30 Go par défaut)
|
|
formaté en **LUKS2**, ouvert par une **clé de 64 octets tirée de `/dev/urandom`**, lisible par
|
|
root seulement (`/root/enervision-coffre.key`, `0400`). Un fichier plutôt qu'une partition : la
|
|
VM n'en a pas de libre, et l'image se déplace ou se sauvegarde comme un fichier.
|
|
- Le mapper `enervision-coffre` porte un ext4 monté sur `/srv/enervision/coffre`, et
|
|
`/var/lib/docker/volumes` est **bind-monté** depuis `/srv/enervision/coffre/docker-volumes`.
|
|
Docker ne voit qu'un dossier ordinaire : ni `data-root`, ni les fichiers Compose, ni les noms
|
|
de volumes ne changent, et les trois environnements sont couverts d'un coup.
|
|
- L'ouverture et les montages sont déclarés dans **`/etc/crypttab` et `/etc/fstab`, avec
|
|
`nofail`** sur les trois lignes : un coffre absent ne doit jamais envoyer la machine en mode
|
|
urgence, où SSH ne répond plus. Sur Debian 13 le générateur crypttab est dans le paquet
|
|
`systemd-cryptsetup`, installé par le script s'il existe dans apt.
|
|
- Un **drop-in `RequiresMountsFor=/var/lib/docker/volumes`** sur `docker.service` fait la
|
|
barrière : sans le bind, Docker ne démarre pas, plutôt que de recréer des volumes vides en
|
|
clair et de laisser trois stacks se lever sur des bases neuves.
|
|
- Le script est **rejouable** : clé, image, formatage, système de fichiers, crypttab, fstab et
|
|
drop-in ne sont posés que s'ils manquent, et il sort sans rien toucher si
|
|
`/var/lib/docker/volumes` est déjà servi par le coffre. La **migration à froid** des volumes
|
|
existants n'a lieu qu'avec `COFFRE_MIGRER=1` : refus si `live-restore` est actif, arrêt de
|
|
`docker.socket` et `docker.service`, `rsync -aHAX --numeric-ids`, comparaison du nombre et de la
|
|
taille des fichiers, puis bascule du dossier et redémarrage de Docker. L'ancien dossier reste en
|
|
`/var/lib/docker/volumes.avant-coffre` jusqu'à validation par un redémarrage.
|
|
- Terraform peut le jouer : `null_resource.coffre`, activé par `coffre_taille` non vide,
|
|
s'exécute après Docker et avant `provision-host.sh`. La ressource est optionnelle et absente du
|
|
plan tant que la variable est vide.
|
|
|
|
**SSE-C sur les archives exportées vers Garage.** Le module d'export du DAG `retention` envoie
|
|
chaque archive avec une clé client (`GARAGE_SSE_KEY`, générée dans le `.env` par
|
|
`provision-host.sh`) ; Garage la chiffre en AES-256-GCM et n'en garde que l'empreinte. Les objets
|
|
sont donc chiffrés une seconde fois, avec une clé distincte de celle du coffre, dans le seul
|
|
dépôt que l'on pourrait un jour sortir de la VM.
|
|
|
|
## Alternatives écartées
|
|
|
|
| Écartée | Raison |
|
|
|---|---|
|
|
| `pgcrypto`, chiffrement par colonne | Ne couvre ni les index, ni les journaux WAL, ni Garage, ni Airflow ; la clé serait dans l'application, à côté des données, pour un coût de développement et de requête sur chaque lecture d'hypertable. |
|
|
| Déplacer le `data-root` de Docker dans le coffre | Chiffre aussi les images et les couches, sans valeur, et impose de recopier tout `/var/lib/docker` : plus long, plus de place, et le démon doit être reconfiguré. Seuls les volumes portent des données. |
|
|
| Chiffrer côté client dans le module d'export | Couvre les archives et rien d'autre, avec une bibliothèque cryptographique à porter dans le code métier alors que Garage offre SSE-C. Retenu seulement sous cette forme, en complément du coffre. |
|
|
| Volume Docker chiffré par un plugin | Un plugin tiers par volume nommé, à installer et suivre sur la machine, pour huit volumes par environnement ; le coffre les couvre tous d'un bind. |
|
|
| Disque ou partition dédiée | La VM n'a qu'un disque virtuel, sans partition libre, et son redimensionnement n'est pas dans les mains de l'équipe. |
|
|
| Clé saisie au démarrage | Personne devant la console ; un redémarrage de la VM par l'école laisserait la plateforme arrêtée jusqu'à intervention. |
|
|
| Clé scellée dans un TPM | La VM n'en expose pas. |
|
|
|
|
## Conséquences
|
|
|
|
- **Ce que le coffre protège, et ce qu'il ne protège pas.** La clé et l'image vivent sur le même
|
|
disque. Le coffre protège une copie isolée de l'image ou du disque : snapshot, sauvegarde,
|
|
décommissionnement du disque virtuel. Il ne protège ni du vol du disque entier, où la clé se
|
|
trouve aussi, ni d'un root sur l'hôte allumé, qui lit le montage en clair. La copie
|
|
`.avant-coffre`, supprimée après validation, n'est pas effaçable physiquement sur un disque
|
|
virtuel. La clé SSE-C transite en clair sur le réseau Compose interne, entre `airflow-scheduler`
|
|
et Garage, à chaque objet envoyé.
|
|
- **Perte de la clé, perte de tout.** Sans `/root/enervision-coffre.key`, l'image est illisible
|
|
et les trois bases avec elle. La clé est à sauvegarder hors de la VM tout de suite après la
|
|
pose, dans un emplacement que seuls les administrateurs lisent.
|
|
- **Coupure lors de la migration.** La copie des volumes se fait Docker arrêté : les trois
|
|
environnements sont indisponibles une à trois minutes, et le disque doit porter deux fois la
|
|
taille des volumes jusqu'à la suppression de `.avant-coffre`.
|
|
- **Redémarrage de test obligatoire.** L'ordonnancement crypttab, fstab, drop-in ne se vérifie
|
|
qu'en redémarrant : `findmnt /var/lib/docker/volumes` et `docker ps` après le reboot, avant de
|
|
supprimer la copie en clair.
|
|
- **Docker dépend du coffre.** Si l'image ou la clé disparaît, Docker refuse de démarrer
|
|
(`dependency failed`) et la machine reste joignable par SSH ; c'est voulu. Retirer la ligne
|
|
fstab du bind retire cette protection sans message.
|
|
- **Rotation.** La clé LUKS se change par `cryptsetup luksChangeKey` sans réécrire les données.
|
|
`GARAGE_SSE_KEY` ne se change pas sans réécrire chaque objet : Garage n'a pas de re-chiffrement
|
|
côté serveur, et un objet écrit avec l'ancienne clé ne se lit qu'avec elle.
|
|
- **Terraform interrompt la stack, une fois.** La première pose avec `coffre_taille` est la seule
|
|
ressource de `vm-eni` qui arrête Docker, en contradiction assumée avec
|
|
l'[ADR 0010](0010-terraform-provisionne-github-actions-deploie.md) pour cette seule occasion ;
|
|
les `apply` suivants trouvent le coffre en place et n'y touchent pas.
|