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).
8.4 KiB
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,
ADR 0017), 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), 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-coffreporte un ext4 monté sur/srv/enervision/coffre, et/var/lib/docker/volumesest bind-monté depuis/srv/enervision/coffre/docker-volumes. Docker ne voit qu'un dossier ordinaire : nidata-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/crypttabet/etc/fstab, avecnofailsur 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 paquetsystemd-cryptsetup, installé par le script s'il existe dans apt. - Un drop-in
RequiresMountsFor=/var/lib/docker/volumessurdocker.servicefait 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/volumesest déjà servi par le coffre. La migration à froid des volumes existants n'a lieu qu'avecCOFFRE_MIGRER=1: refus silive-restoreest actif, arrêt dedocker.socketetdocker.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-coffrejusqu'à validation par un redémarrage. - Terraform peut le jouer :
null_resource.coffre, activé parcoffre_taillenon vide, s'exécute après Docker et avantprovision-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, entreairflow-scheduleret 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/volumesetdocker psaprè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 luksChangeKeysans réécrire les données.GARAGE_SSE_KEYne 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_tailleest la seule ressource devm-eniqui arrête Docker, en contradiction assumée avec l'ADR 0010 pour cette seule occasion ; lesapplysuivants trouvent le coffre en place et n'y touchent pas.