#!/usr/bin/env bash # Pourquoi : les volumes Docker nommés des trois environnements (pgdata TimescaleDB, Garage, # Airflow, Prometheus, Grafana) vivent en clair sous /var/lib/docker/volumes ; Garage n'a pas de # chiffrement côté serveur et PostgreSQL communautaire n'a pas de TDE (issue #42, ADR 0020). # coffre-luks.sh pose un coffre LUKS2 dans un fichier image creux, le monte, puis bind-monte # /var/lib/docker/volumes depuis ce coffre : les trois projets Compose sont chiffrés au repos # sans qu'un fichier Compose change. La clé vit sur le même disque que l'image : le coffre # protège une copie isolée de l'image ou du disque (snapshot, sauvegarde, décommissionnement), # pas le vol du disque entier ni un root sur l'hôte allumé, qui lit le montage en clair. # Piège : le drop-in RequiresMountsFor sur docker.service est la seule barrière qui empêche # Docker de recréer des volumes en clair si le coffre manque au démarrage ; retirer la ligne # fstab du bind la désactive sans message. `nofail` partout, sinon un coffre absent envoie la # machine en mode urgence et coupe SSH. Bind et drop-in ne sont posés qu'avec la migration : # posés avant, un redémarrage masquerait les volumes en clair sous un coffre vide. Rejouable. set -euo pipefail COFFRE_IMAGE="${COFFRE_IMAGE:-/srv/enervision/coffre.img}" COFFRE_CLE="${COFFRE_CLE:-/root/enervision-coffre.key}" COFFRE_MONTAGE="${COFFRE_MONTAGE:-/srv/enervision/coffre}" COFFRE_TAILLE="${COFFRE_TAILLE:-30G}" COFFRE_MIGRER="${COFFRE_MIGRER:-0}" MAPPER="enervision-coffre" PERIPHERIQUE="/dev/mapper/$MAPPER" VOLUMES="/var/lib/docker/volumes" SOURCE_BIND="$COFFRE_MONTAGE/docker-volumes" DROPIN="/etc/systemd/system/docker.service.d/enervision-coffre.conf" APT_A_JOUR=0 erreur() { echo "erreur : $*" >&2; exit 1; } if [[ $# -gt 0 ]]; then echo "Usage : [COFFRE_TAILLE=30G] [COFFRE_MIGRER=1] [COFFRE_IMAGE=...] [COFFRE_CLE=...] [COFFRE_MONTAGE=...] $0" >&2 exit 2 fi installer() { local paquet="$1" dpkg -s "$paquet" >/dev/null 2>&1 && return 0 if [[ $APT_A_JOUR -eq 0 ]]; then apt-get update -qq APT_A_JOUR=1 fi apt-cache show "$paquet" >/dev/null 2>&1 || return 1 DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends "$paquet" >/dev/null echo "$paquet installé" } verifier_prerequis() { [[ "$(id -u)" -eq 0 ]] || erreur "à lancer en root" # Constaté le 24/09 : la machine ENI est un conteneur LXC, sans device-mapper ni loop. LUKS y # est impossible ; le chiffrement de son disque relève de l'hôte Proxmox (ADR 0020). [[ "$(systemd-detect-virt --container 2>/dev/null || true)" != lxc ]] \ || erreur "conteneur LXC : pas de device-mapper ni de loop, LUKS impossible ici ; le chiffrement du disque se fait sur l'hôte (ADR 0020)" [[ -e /dev/mapper/control ]] || erreur "/dev/mapper/control absent : device-mapper indisponible, LUKS impossible ici" [[ "$COFFRE_IMAGE" != /var/lib/docker/* ]] \ || erreur "l'image $COFFRE_IMAGE ne doit pas vivre sous /var/lib/docker, que le coffre recouvre" installer cryptsetup || erreur "cryptsetup introuvable dans apt" # Debian 13 sépare le générateur crypttab dans systemd-cryptsetup ; sans lui, crypttab est ignoré. installer systemd-cryptsetup || echo "systemd-cryptsetup absent d'apt : le générateur crypttab est dans systemd" installer rsync || erreur "rsync introuvable dans apt" for outil in truncate blkid findmnt lsblk mkfs.ext4 systemctl; do command -v "$outil" >/dev/null || erreur "$outil absent" done } deja_sur_le_coffre() { [[ "$(findmnt -n -o SOURCE "$VOLUMES" 2>/dev/null || true)" == *"$MAPPER"* ]] } poser_cle() { if [[ ! -f "$COFFRE_CLE" ]]; then (umask 077 && head -c 64 /dev/urandom > "$COFFRE_CLE") echo "clé générée : $COFFRE_CLE" fi chmod 400 "$COFFRE_CLE" } poser_image() { if [[ ! -f "$COFFRE_IMAGE" ]]; then mkdir -p "$(dirname "$COFFRE_IMAGE")" (umask 077 && truncate -s "$COFFRE_TAILLE" "$COFFRE_IMAGE") echo "image creuse créée : $COFFRE_IMAGE ($COFFRE_TAILLE)" fi if ! cryptsetup isLuks "$COFFRE_IMAGE"; then [[ -z "$(blkid -p -o value -s TYPE "$COFFRE_IMAGE" 2>/dev/null || true)" ]] \ || erreur "$COFFRE_IMAGE porte déjà des données hors LUKS, refus de le formater" cryptsetup luksFormat --type luks2 --batch-mode --key-file "$COFFRE_CLE" "$COFFRE_IMAGE" echo "image formatée en LUKS2" fi if [[ ! -e "$PERIPHERIQUE" ]]; then cryptsetup open --key-file "$COFFRE_CLE" "$COFFRE_IMAGE" "$MAPPER" fi if [[ -z "$(blkid -p -o value -s TYPE "$PERIPHERIQUE" 2>/dev/null || true)" ]]; then mkfs.ext4 -q -L "$MAPPER" "$PERIPHERIQUE" echo "système de fichiers ext4 créé dans le coffre" fi mkdir -p "$COFFRE_MONTAGE" if ! findmnt -n -M "$COFFRE_MONTAGE" >/dev/null; then mount "$PERIPHERIQUE" "$COFFRE_MONTAGE" fi mkdir -p "$SOURCE_BIND" } fstab_contient() { local cible="$1" awk -v cible="$cible" '$1 !~ /^#/ && $2 == cible { trouve = 1 } END { exit !trouve }' /etc/fstab } poser_persistance() { touch /etc/crypttab if ! awk -v nom="$MAPPER" '$1 == nom { trouve = 1 } END { exit !trouve }' /etc/crypttab; then echo "$MAPPER $COFFRE_IMAGE $COFFRE_CLE luks,nofail" >> /etc/crypttab echo "crypttab : $MAPPER ajouté" fi if ! fstab_contient "$COFFRE_MONTAGE"; then echo "$PERIPHERIQUE $COFFRE_MONTAGE ext4 defaults,nofail,x-systemd.device-timeout=30s 0 2" >> /etc/fstab echo "fstab : $COFFRE_MONTAGE ajouté" fi systemctl daemon-reload } poser_bind() { if ! fstab_contient "$VOLUMES"; then echo "$SOURCE_BIND $VOLUMES none bind,nofail 0 0" >> /etc/fstab echo "fstab : bind de $VOLUMES ajouté" fi if [[ ! -f "$DROPIN" ]]; then mkdir -p "$(dirname "$DROPIN")" cat > "$DROPIN" </dev/null | grep -q "Live Restore Enabled: true"; then erreur "live-restore actif : les conteneurs survivraient à l'arrêt du démon, volumes en clair ouverts. Le désactiver dans /etc/docker/daemon.json avant de migrer" fi [[ "$(du -sb "$VOLUMES" | cut -f1)" -lt "$(libre "$COFFRE_MONTAGE")" ]] \ || erreur "le coffre est trop petit pour $VOLUMES ($(du -sh "$VOLUMES" | cut -f1)) : relancer avec une image plus grande" echo "arrêt de Docker : les trois environnements sont coupés le temps de la copie" systemctl stop docker.socket docker.service poser_bind rsync -aHAX --numeric-ids "$VOLUMES/" "$SOURCE_BIND/" origine="$(empreinte "$VOLUMES")" copie="$(empreinte "$SOURCE_BIND")" [[ "$origine" == "$copie" ]] \ || erreur "copie incomplète : $origine dans $VOLUMES, $copie dans $SOURCE_BIND. Docker est arrêté, rien n'a été déplacé" echo "copie vérifiée : $copie" mv "$VOLUMES" "$VOLUMES.avant-coffre" mkdir "$VOLUMES" if ! mount "$VOLUMES" || ! deja_sur_le_coffre; then umount "$VOLUMES" 2>/dev/null || true rmdir "$VOLUMES" mv "$VOLUMES.avant-coffre" "$VOLUMES" erreur "bind impossible à monter depuis le coffre : $VOLUMES remis en place, Docker reste arrêté" fi systemctl start docker.socket docker.service docker volume ls } afficher_etat() { echo losetup -j "$COFFRE_IMAGE" 2>/dev/null || true lsblk "$PERIPHERIQUE" 2>/dev/null || true findmnt "$VOLUMES" || echo "$VOLUMES n'est pas un point de montage" cat < 2. Redémarrer la machine pour valider l'ordonnancement crypttab, fstab, docker, puis vérifier : findmnt $VOLUMES && docker ps FIN if [[ -d "$VOLUMES.avant-coffre" ]]; then cat <