Introduction

Ce guide décrit l'installation d'un système Debian GNU/Linux sur un disque entièrement chiffré, avec stockage de la clé de chiffrement dans le TPM (Trusted Plateform Module) du PC, avec support de SecureBoot. L'utilisation combinée du TPM et de SecureBoot permet de déchiffrer le disque au démarrage sans interaction utilisateur (pas de saisie de phrase de passe, ni de dispositif externe à connecter) tout en garantissant que la clé de déchiffrement ne sera fournie qu'au système approuvé : celui que nous allons installer.

L'installation présentée ici se base sur des choix différents de ceux de l'installateur Debian officiel, ce qui impliquera des différences de configuration, mais l'ensemble de ce qui sera installé provient bien des dépôts de paquets de Debian, et les mises à jour se feront de la manière habituelle avec les utilitaires Debian.

Matériel nécessaire

Cette installation nécessite un PC avec micrologiciel UEFI et support de SecureBoot, et équipé d'un module TPM 2.0. Ces caractéristiques étant exigées par Microsoft, tout PC commercialisé avec Windows 10 ou supérieur devrait en principe les satisfaire.

Il n'y a pas besoin de support spécifique à l'installation, mais l'essentiel des manipulations devront se faire depuis un système GNU/Linux opérationnel sur la machine de destination. Un système live ou un système déjà installé sur la même machine (mais sur un autre disque) peuvent convenir. L'ensemble des commandes sont à exécuter en tant que root, cela n'est donc pas systématiquement précisé.

Sources

Le contenu de ce guide est en grande partie basé sur plusieurs sources anglophones :

D'une manière générale, il est tout à fait possible que j'aie introduit des erreurs qui ne sont pas présentes dans ces sources.

Partitionnement du disque

La première étape consiste à préparer le disque pour l'installation en y créant les partitions nécessaires. À ce stade, il n'est pas indispensable de travailler sur la machine cible, il est tout à fait possible de partitionner le disque depuis un autre PC. Il faudra toutefois faire attention au fait que le nommage du périphérique peut être différent selon la machine et le mode de connexion. On suppose ici que l'installation se fait sur un unique disque, vierge au départ, et que l'on ne souhaite pas y installer d'autre système.

Avant tout, il faut identifier le disque sur lequel on va travailler. La commande lsblk permet de voir l'ensemble des disques présents sur le système, ainsi que l'arborescence de leurs partitions. Dans mon cas, j'installe le système sur l'unique disque NVMe d'un PC portable. Le périphérique est donc nommé /dev/nvme0n1, c'est ce nom qui sera utilisé dans l'ensemble des exemples.

Création de la table de partition

On utilise fdisk pour créer les partitions :

fdisk /dev/nvme0n1

Puis on suit les étapes suivantes :

  • créer une table de partition de type GPT (commande g)
  • créer une première partition de type EFI de 512 Mio (on verra plus loin pourquoi cette taille est nécessaire) :
    • commande n pour créer la partition,
    • numéro de partition et premier secteur par défaut (valider sans rien entrer),
    • pour le dernier secteur saisir +512M,
    • définir le type de partition : commande t puis saisir la valeur 1.
  • créer une seconde partition qui contiendra l'ensemble du système :
    • commande n pour créer la partition,
    • utiliser l'ensemble de l'espace restant en validant toutes les questions sans rien saisir,
    • le type par défaut Linux filesystem convient, on ne le modifie pas.
  • quitter fdisk en enregistrant les changements avec la commande w.
Attention : cette étape écrasera une éventuelle table de partition précédemment existante, rendant les données présentes difficilement accessibles par la suite, assurez-vous donc d'avoir bien sauvegardé ce qui doit l'être. Toutefois, la simple création d'une table de partition ne garantit pas la suppression réelle des données (la question de l'effacement sécurisé des données n'est pas traitée ici).

Création du système de fichier EFI

Il reste à créer le système de fichier de la partition EFI, qui doit être de type FAT 32. On le fait avec la commande suivante :

mkfs.vfat -F 32 /dev/nvme0n1p1

Création de la partition système

Pour accueillir dans cette unique partition l'ensemble du système, le tout étant chiffré, il va falloir superposer plusieurs couches logicielles. La mise en place se divise donc en plusieurs étapes, correspondant à ces différentes couches :

  • une partition LUKS (Linux Unified Key Setup) qui servira de conteneur chiffré pour les couches supérieures,
  • une couche LVM (Logical Volume Management) pour pouvoir créer plusieurs volumes logiques dans un conteneur LUKS unique,
  • un système de fichier dans chaque volume logique.

Avant de détailler ces étapes, assurons-nous que nous avons bien installé tous les programmes nécessaires :

apt install cryptsetup lvm2 btrfs-progs

Contrairement à ce que l'on pourrait penser, il est parfaitement possible d'installer des paquets supplémentaires sur un système live, cette installation ne sera simplement pas persistente.

Création du conteneur chiffré

On commence par créer la partition LUKS avec la commande suivante :

cryptsetup luksFormat --type luks2 --verify-passphrase /dev/nvme0n1p2

Notez qu'à ce stade, il n'est pas encore question de l'utilisation du TPM. La clé de chiffrement est protégée, de façon classique, par une phrase de passe qu'il faudra saisir pour déchiffrer la partition. Le déchiffrement automatique par le TPM sera mis en place plus tard car cela doit être fait depuis le système que nous nous apprêtons à installer, et donc lorsque celui-ci sera opérationnel et que nous aurons pu démarrer dessus.

Même si elle ne sera plus utilisée au quotidien, la phrase de passe définie ici restera toutefois en place et servira de clé de secours, en cas de problème. Par exemple si le système ne démarre plus et que l'on a besoin de démarrer depuis un autre support (une clé USB live, par exemple). Il est donc important de bien la choisir : elle ne doit pas être trop simple pour ne pas affaiblir le chiffrement du disque, mais il faudra pouvoir s'en souvenir, alors même qu'on ne l'utilisera pas au quotidien.

On va ensuite ouvrir la partition LUKS :

cryptsetup open /dev/nvme0n1p2 nvme0n1p2_crypt

Le dernier paramètre de cette commande est un nom totalement arbitraire que l'on donne au volume déchiffré. Du point de vue du fonctionnement de LUKS, ce nom n'est pas enregistré et pourrait donc changer à chaque nouvelle ouverture. Il sera toutefois utilisé plus loin dans une configuration persistente, il faut donc choisir tout de suite un nom qui sera conservé ensuite. Utiliser le nom du volume sous-jacent suffixé par _crypt semble être une convention courante, mais cela n'a rien d'obligatoire.

Le contenu en clair de la partition est désormais accessible dans /dev/mapper/nvme0n1p2_crypt.

Mise en place de la gestion de volumes logiques

L'étape suivante est d'initialiser la gestion des volumes logiques. Pour cela, on va d'abord définir notre partition en tant que Volume Physique (PV, d'après les initiales anglaises) :

pvcreate /dev/mapper/nvme0n1p2_crypt

On crée ensuite un Groupe de Volumes (VG) constitué de notre unique PV. Contrairement au PV, nous devons nommé notre VG. Là encore le nom est arbitraire, mais choisir une convention de nommage et s'y tenir permet de s'y retrouvé par la suite. C'est d'autant plus important que l'on ne manipulera pas ce nom quotidiennement, il faut donc choisir un nommage qui sera facile à comprendre le jour ou on devra y remettre le nez après plusieurs mois ou années. Sur un PC portable ou de bureau, il est assez peu probable d'avoir plusieurs VG sur la même machine, je nomme donc tout simplement mes VG d'après le nom d'hôte préfixé par vg_. Si la machine doit s'appeler, par exemple, rocannon, cela donne :

vgcreate vg_rocannon /dev/mapper/nvme0n1p2_crypt

On peut maintenant créer les Volumes Logiques (LV) qui accueilleront les différents systèmes de fichiers dont nous avons besoin. Le nombre de LV dépend du choix du type de système de fichier : j'utilise ici une unique partition Btrfs (qui sera organisée en sous-volumes à l'étape suivante) et une partition d'échange (swap). Je crée donc les deux LV correspondant :

lvcreate -n lv_swap -L 8G vg_rocannon
lvcreate -n lv_racine -l 100%FREE vg_rocannon

Là encore, les noms sont totalement arbitraires. La remarque faite plus haut sur la pertinence de suivre une convention de nommage cohérente reste valable, ici j'utilise le préfixe lv_ suivi d'un mot décrivant l'usage du volume.

Notez la nuance entre les options -L, qui indique la taille absolue, et -l, qui permet d'exprimer une taille en fonction de l'espace libre restant (et aussi d'autres possibilités, cf. la page de manuel de lvcreate).

Je ne traite pas ici de la question de la pertinence d'une partition de swap sur une machine moderne avec beaucoup de mémoire physique, ni de la question de sa taille. Sur ce sujet on peut se référer, entre autre, à cet article.

Création des systèmes de fichiers

Ça n'est pas, à proprement parler, un système de fichier, mais la création de l'espace d'échange vient assez naturellement à ce stade du processus :

mkswap /dev/vg_rocannon/lv_swap

On crée ensuite le système de fichier btrfs :

mkfs.btrfs /dev/vg_rocannon/lv_racine

Un des avantages de Btrfs est la possibilité de créer des sous-volumes au sein du même système de fichier. Je trouve ce sytème plus flexible que LVM pour séparer, par exemple, /home de la racine /. C'est la raison pour laquelle j'ai créé un seul LV avec un seul système Btrfs. Nous alons maintenant créer les sous-volumes, ce qui nécessite d'abord de monter (temporairement) la partition. On le fait ici directement dans le /mnt du système depuis lequel on travaille :

mount /dev/vg_rocannon/lv_racine /mnt
cd /mnt
btrfs subvolume create racine
btrfs subvolume create home
umount /mnt

Je n'ai créé ici de sous-volume que pour la racine et /home. Si vous souhaitez séparer de la racine d'autres points de montage, comme /usr, /var ou /tmp, il faut aussi leur créer chacun son sous-volume. On ne fixe pas de taille à ce stade, mais Btrfs permet de fixer des quotas pour les sous-volumes, je ne détaille pas cet aspect dans ce guide, cela est bien documenté, par exemple dans la page de manuel de la commande btrfs-quota.

Bilan d'étape

Cette première phase de préparation est maintenant terminée, et nous allons pouvoir passer à l'installation proprement dite. Avant cela, je discute un peu plus, ci-dessous, des choix faits en terme de partitionnement. Vous pouvez sans problème sauter cette partie et passer au chapitre suivant.

On pourrait s'attendre à ce que le choix d'utiliser les sous-volumes de Btrfs permette de se passer totalement de la couche LVM, ce qui aurait simplifié l'ensemble. C'est uniquement la partition d'échange qui rend ici LVM nécessaire. Plusieurs solutions auraient permis d'éviter cela, mais aucune ne me semble satisfaisante :

  • la première est de mettre la swap dans sa propre partition directement sur le disque mais il aurait alors fallu lui créer son propre volume LUKS. En effet, un système chiffré mais avec une swap en clair ne servirait plus à rien : il serait tout à fait possible que des données sensibles, (y compris la clé de chiffrement du volume LUKS) aterrissent dans la swap, et soient donc accessibles en clair. Cela aurait aussi rendu moins flexible une éventuelle modification de la taille de la swap, et le processus de déchiffrement au démarrage aurait été un peu plus complexe.
  • la seconde est de ne pas avoir de swap du tout, mais d'après l'article cité plus haut (dont l'auteur est un des principaux développeurs du noyau Linux…), ça n'est pas une bonne idée. Mais si vous choisissez de vous passer totalement de swap, vous pouvez alors sauter l'étape LVM et créer directement le système Btrfs à la place du PV.
  • la troisième est d'utiliser un fichier de swap, plutôt qu'une partition. C'est sans doute une solution beaucoup plus flexible : non seulement elle nous aurait permit d'éviter la couche LVM, mais elle permet de modifier plus facilement la taille de la swap si nécessaire. Malheureusement, les fichiers de swap s'accomodent mal du system de Copy-on-Write qui est à la base de Btrfs (voir à ce sujet la documentation de Btrfs). Choisir un autre système de fichier, comme ext4, aurait permit d'utiliser un fichier de swap, mais cela aurait aussi nécessité de réintroduire LVM pour pouvoir séparer /home de la partition racine. Et même dans ce cas, les fichiers de swap ont la réputation d'être moins performants, mais surtout moins largement utilisés, donc moins testés et moins fiables.

Système de base

L'installation du système de base de Debian se fait grace au script debootstrap, fourni par le paquet du même nom. Pour l'utiliser, nous devons d'abord monter les deux sous-volumes Btrfs créés à l'étape précédente, en respectant la hierarchie des répertoires.

Préparation des répertoires

On commence par le système racine :

mount -o subvol=racine /dev/vg_rocannon/lv_racine /mnt

Notez l'option subvol qui fait qu'on ne monte pas l'ensemble du volume Btrfs, mais seulement le sous-volume spécifié. On va ensuite créer le répertoire /home à la racine de ce sous-volume, pour ensuite y monter le sous-volume correspondant :

mkdir /mnt/home
mount -o subvol=home /dev/vg_rocannon/lv_racine /mnt/home

Nous avons désormais nos sous-volumes montés dans /mnt conformément à la structure de répertoires classique. Si vous avez créé d'autres sous volumes pour /var, /usr ou /tmp, il est temps de les monter de la même façon. En fait, l'installation du système de base ne créant en principe rien dans /home, le monter tout de suite n'était pas forcément indispensable. C'est par contre obligatoire pour /usr ou /var. Concernant /tmp, j'ai l'habitude d'en faire un système de fichier en mémoire, je le crée donc tout de suite, pour éviter que d'éventuels fichiers temporaires de l'installation ne viennent se créer sur le disque, et soient ensuite masqués par le montage du tmpfs. Là encore, ça n'est probablement pas indispensable à ce stade.

mkdir /mnt/tmp
mount -t tmpfs -o size=2G tmpfs /mnt/tmp

On monte enfin la partition EFI à son emplacement habituel :

mkdir -p /mnt/boot/efi
mount /dev/nvme0n1p1 /mnt/boot/efi

Installation du système de base

Pour l'installation, on va simplement indiqué à debootstrap la version de Debian à utiliser, le répertoire cible (correspondant à la racine du futur système), et enfin l'adresse du dépôt depuis lequel télécharger les paquets :

debootstrap trixie /mnt https://deb.debian.org/debian/

À l'heure ou j'écris ces lignes, Debian Trixie est encore la version de test, mais certains outils utilisés ici ne sont pas disponible dans la version stable actuelle (Bookworm). L'installation décrite ici ne sera donc probablement possible pour une Debian stable qu'une fois que Trixie aura été publiée en tant que version stable Debian 13.

Configuration minimale

Maintenant que le système de base est installé, nous allons pouvoir y exécuter un shell grâce à chroot, et y créer la configuration minimale nécessaire pour pouvoir ensuite le démarrer.

Préparation du chroot

En plus des fichiers créés par debootstrap, le système a besoin d'un certain nombre de fichiers qui n'existent pas réellement sur le disque, mais représentent, entre autre, les moyens d'accès au matériel ou divers états interne du système. Il s'agit du contenu des répertoires /proc, /dev, /sys et /run, ainsi que certains de leurs sous-répertoires. Nous allons donc monter ces répertoires dans le système en cours d'installation depuis le système hôte :

mount -t proc none /mnt/proc
mount -o bind /dev /mnt/dev
mount -o bind /dev/pts /mnt/dev/pts
mount -o bind /sys /mnt/sys
mount -o bind /sys/firmware/efi/efivars /mnt/sys/firmware/efi/efivars
mount -o bind /run /mnt/run

Nous avons désormais une hierarchie de répertoires complète dans /mnt, et nous pouvons donc y démarrer un shell avec chroot :

chroot /mnt /bin/bash

Configuration

Nous sommes maintenant dans un bash qui s'exécute dans le contexte de notre système en cours d'installation, nous allons donc pouvoir mettre en place la configuration minimale nécessaire au démarrage.

On commence par installer un certain nombre de paquets nécessaires au démarrage. J'y ajoute neovim, par préférence personnelle, l'important est d'avoir un éditeur qui vous convient (par défaut c'est nano qui est installé avec Debian) :

apt-get update
apt-get install cryptsetup lvm2 btrfs-progs adduser console-setup locales neovim

Montage des systèmes de fichiers

Le principal point de configuration à ce stade est le montage des systèmes de fichiers que nous avons créés, on va d'abord définir notre volume chiffré dans /etc/crypttab avec la ligne suivante :

# <target name>     <source device>     <key file>  <options>
nvme0n1p2_crypt     /dev/nvme0n1p2      none        luks,discard

Maintenant que le volume chiffré est défini, on peut configurer les différents systèmes de fichiers dans /etc/fstab :

# <file system>            <mount point> <type>  <options>               <dump>  <pass>
/dev/vg_rocannon/lv_racine /             btrfs   defaults,subvol=racine  0       1
/dev/vg_rocannon/lv_racine /home         btrfs   defaults,subvol=home    0       2
/dev/nvme0n1p1             /boot/efi     vfat    umask=0077              0       1
/dev/vg_rocannon/lv_swap   none          swap    sw                      0       0
tmpfs                      /tmp          tmpfs   defaults,size=2g        0       0

Dans ces deux fichiers, on a ici identifié les périphériques (deuxième colonne de crypttab et première de fstab) par leur chemin dans /dev, mais il est préférable de les identifier par leur UUID, les numéros dans les noms de périphériques étant susceptibles de changer en cas d'ajout ou de retrait de matériel (les noms de volumes LVM sont toutefois plus stables).

Une façon pratique d'obtenir la valeur de l'UUID au bon format est d'utiliser la commande suivante :

blkid | grep 'crypto_LUKS' | cut -d ' ' -f 2 | tr -d '"'

La valeur recherchée par grep est bien sûr à adapter. La chaîne crypto_LUKS utilisée ici retournera le (ou les, s'il y a plusieurs volumes chiffrés) UUID à utiliser dans le fichier crypttab.

Création des comptes utilisateurs

L'installation de base par debootstrap ne crée aucun compte utilisateur, et ne configure pas non plus de mot de passe pour le compte root. Au démarrage, nous serions donc dans l'impossibilité de nous connecter. Il n'est pas indispensable de créer tout de suite l'ensemble des comptes utilisateurs dont on aura besoin au final, mais il faut a minima disposer d'un compte avec les droits d'administration. Deux choix sont possibles :

  • soit créer un mot de passe pour root, il suffit pour cela d'utiliser la commande passwd,
  • soit créer un compte utilisateur avec adduser, et lui donner les droits d'administration en l'ajoutant au groupe sudo :
    adduser toto
    apt-get install sudo
    adduser toto sudo
    

Mise en place des clés Secure-Boot

Secure-Boot est une fonctionnalité associée à UEFI qui vise à ne permettre de démarrer un système que s'il est signé par une clé cryptographique connues du micrologiciel.

Le mode d'installation par défaut de Debian (et de la plupart des autres distributions) repose, pour le démarrage, sur un enchaînement de plusieurs étapes pour aboutir au démarrage d'un noyau Linux qui n'est pourtant pas directement signé par une clé acceptée par Secure-Boot. Le premier élément à être chargé est nommé Shim, il s'agit d'un chargeur d'amorçage (ou bootloader) minimal qui a pour fonction de passer le relais à un autre exécutable UEFI, après avoir vérifier qu'il était autorisé. Shim est autorisé à démarrer parce qu'il est signé par Microsoft, et que la clé publique correspondant à la signature est installée par défaut par tous les principaux constructeurs. Il embarque à son tour les clés des principales distributions Linux, ce qui lui permet de vérifier les différents systèmes qu'il charge dans un deuxième temps. Dans la plupart des cas, Shim charge un second chargeur d'ammorçage, plus généraliste, comme Grub, qui charge à son tour le noyau Linux.

Le fonctionnement proposé ici est assez différent : l'idée est de prendre véritablement possession de notre machine en y installant nos propres clés, ce qui nous permettra ensuite de signer nous même directement notre noyau, et donc de le démarrer directement.

Organisation des différents types de clés

Secure-Boot est basé sur la cryptographie asymétrique : les clés vont par deux, l'une privée l'autre publique. La clé privée sert à signer des fichiers (exécutables UEFI, autres clés...) et la clé publique associée permet de vérifier la signature. Le micrologiciel UEFI n'ayant pas pour fonction de signer quoi que ce soit, mais seulement de vérifier des signatures, il ne comporte donc que des clés publiques. Ces clés sont stockées dans différentes variables UEFI :

  • db, la base de données des clés : elle contient les clés publiques qui sont utilisés pour vérifier les systèmes et autoriser leur démarrage. Les clés privées associées sont donc celle qui doivent être utilisées pour signer les exécutables.
  • dbx, la base de données d'interdiction : elle peut contenir des clés qui ont été révoquées (suite à une compromission de la clé privée associée, par exemple) mais aussi des valeurs de hachages correspondant soit à des programmes malveillants qui serait parvenus à être signés par des clés légitimes, soit à des logiciels légitimes, et donc signés, dans lesquels auraient été découvertes des vulnérabilités qui permettrait de les détourner à des fins malveillantes.
  • KEK (pour Key Exchange Key), les clés d'échange de clés : il s'agit de clés dont le rôle est de vérifier les mises à jour de db et de dbx.
  • PK (pour Platform Key), la clé de plateforme : il s'agit de la clé de plus haut niveau, émise en principe par le constructeur de la machine, et qui permet de vérifier les mises à jour des clés d'échange de clés. Contrairement aux précédentes, cette clé est nécessairement unique.

Ces différents types de clés organisent donc une hierarchie dans laquelle chaque niveau a le pouvoir de modifier le niveau strictement inférieur. Seule exception : la PK, étant au plus au niveau, peut se mettre à jour elle-même, c'est à dire qu'une nouvelle PK sera acceptée, et remplacera la PK actuelle, si elle est vérifiée par cette dernière.

Extraction des clés existantes

Même si le système installé sera signé par notre propre clés, il peut être utile de conserver les clés installées par défaut, ou du moins une partie d'entre elles. Par exemple, pour pouvoir démarrer une image live téléchargée depuis le site d'une distribution, il faut conserver la clé Microsoft utilisée pour signer shim.

Pour cela, nous allons commencer par extraire l'ensemble des clés installées par défaut. Cela permet d'une part de les sauvegarder, pour pouvoir les restaurer en cas de problème, et d'autre part d'inclure celles que nous souhaitons conserver dans l'ensemble de clés que nous installeront à l'étape suivante. À partir de maintenant, nous allons interagir avec l'UEFI, puis avec le TPM, il est donc désormais impératif de travailler sur la machine cible.

Les clés étant stockées dans des variables UEFI, leur lecture se fera avec la commande efi-readvar, fournie par le paquet efitools. On indique le nom de la variable à lire avec l'option -v et le fichier de sortie avec -o (sans cette option, on obtient une description du contenu sur la sortie standard). Les fichiers sont au format esl pour Efi Signature List.

efi-readvar -v PK -o PK.esl
efi-readvar -v KEK -o KEK.esl
efi-readvar -v db -o db.esl
efi-readvar -v dbx -o dbx.esl

Chaque fichier contient donc une liste de signature (même si par principe la liste PK ne comporte qu'un seul élément). Pour pouvoir gérer les différentes clés de façon individuelle, on va maintenant convertir ces listes en fichiers unitaire, au format OpenSSL, grâce à la commande sig-list-to-certs, également fournie par le paquet efitools :

sig-list-to-certs KEK.esl KEK
sig-list-to-certs db.esl db

Le premier argument est le nom du fichier, et le second le nom de base pour les fichiers créés. On ne converti pas la PK, puisque celle-ci est nécessairement unique, et que nous allons la remplacer par la nôtre. Il n'est probablement pas utile non plus de convertir le contenu de dbx car nous n'avons rien à y ajouter et qu'il est certainement préférable de conserver l'ensemble de son contenu. Ça n'est donc utile que pour KEK et db, dans le cas où vous souhaitez n'en conserver qu'une partie, par exemple pour conserver la clé avec laquelle Microsoft signe shim, et donc indirectement les différentes distributions Linux, mais pas celle qui sert à signer Windows.

Les clés sont extraites au format DER, nous allons les convertir au format PEM dont nous aurons besoin à l'étape suivante. Cela peut aussi être l'occasion de renommer les fichiers pour mieux s'y retrouver. Par exemple, si la seconde KEK (numérotée 1, du fait du démarrage à 0) est celle de Microsoft :

openssl x509 -in KEK-1.der -out KEK-Microsoft.crt -outform PEM

Génération des clés

Nous allons maintenant générer nos propres clés UEFI, qui nous permettront de signer notre noyau. Il y a 3 clés à générer, une pour chaque type : PK, KEK et db. La génération se fait avec OpenSSL, par exemple pour la PK :

openssl req -new -x509 -newkey rsa:2048 -subj "/CN=<nom-de-la-clé>/" \
    -keyout PK.key -out PK.crt -days 10950 -noenc

Le nom indiqué après CN= permettra d'identifier la clé, il sera affiché dans le menu de l'UEFI ou dans la sortie de la commande efi-readvar. Vous pouvez indiquer ce que vous voulez, tant que vous vous y retrouvez. À titre d'exemple, j'indique mon nom pour les PK et KEK, et le nom de la machine pour la db, et je suffixe dans tous les cas par le type de clé (PK, KEK ou db).

Les clés privées (le fichier PK.key dans l'exemple ci-dessus) doivent impérativement être conservées en sureté. Idéalement, elles devraient rester sur la machine qui les a générées, avec des droits d'accès restreint. Une bonne façon de faire est de les générer dans un sous-répertoire dédié de /root et de restreindre tous les droits sur le répertoire et sur les fichiers de clés privées au seul propriétaire (qui est donc root).

À noter que l'UEFI n'a besoin que des clés publiques. Pour signer le noyau lors de son installation, la clé privée db devra être présente sur le système cible. En revanche, les clés privées PK et KEK ne sont nécessaires que lors de la mise en place initiale (et lors d'une éventuelle future mise à jour de clé), elles peuvent donc être générées sur une autre machine, et seules les clés publiques auront besoin d'être transférer sur le système en cours d'installation.

Si vous comptez suivre cette procédure pour plusieurs machines, il est donc possible de ne générer qu'une seule PK et KEK pour l'ensemble. Il faudra en revanche créer une clé db pour chaque machine (il serait fondamentalement possible d'utiliser la même clé sur plusieurs machines, mais cela impliquerait de copier la clé privée sur chaque machine, ce qui signifie qu'en cas de compromission d'une des machines, il faudrait changer la clé sur toutes les autres).

Maintenant que nous avons créé nos clés, il faut les convertir au format esl qui est celui attendu par l'UEFI. On commence par convertir individuellement chaque clé publique avec cert-to-efi-sig-list. Cette commande admet une option -g qui permet de fournir un identifiant de propriétaire de clé, au format UUID. On va donc d'abord générer, avec la commande uuid cet identifiant, que l'on utilisera ensuite pour l'ensemble de nos clés.

guid=$(uuid)
cert-to-efi-sig-list -g $guid PK.crt PK.esl

Pour la PK, qui est par principe unique, il n'y a rien d'autre à faire. Mais pour les KEK et db, nous allons obtenir un fichier esl pour chaque clé. Le format esl permet de faire cela très simplement, il suffit de concaténer les fichiers :

cat KEK1.esl KEK2.esl KEK3.esl > KEK.esl

Si vous souhaitez conserver toutes les clés préinstallées, et simplement ajouter la vôtre, vous pouvez donc concaténer simplement votre fichier esl avec celui extrait précédement avec efi-readvar. Si par contre vous souhaitez sélectionner seulement certaines clés, il faudra re-convertir les certificats obtenus avec sig-list-to-certs en fichier esl individuel, puis les concaténer. Dans ce cas, il est probablement préférable, en tout cas plus rigoureux, de conserver pour ces fichiers le guid propriétaire d'origine. Celui-ci est afficher dans la sortie par défaut (sans option -o) de la commande efi-readvar.

Signature des clés

Nous avons désormais nos 3 fichiers de clés : PK.esl, KEK.esl et db.esl. Il nous reste à les signer selon la hierarchie présentée plus haut : la PK est signée par elle même, la KEK par la PK, et la db par la KEK. Les fichiers signés sont nommés avec l'extension .auth :

sign-efi-sig-list -t "$(date +'%Y-%m-%d %H:%M:%S')" -k PK.key -c PK.crt PK PK.esl PK.auth
sign-efi-sig-list -t "$(date +'%Y-%m-%d %H:%M:%S')" -k PK.key -c PK.crt KEK KEK.esl KEK.auth
sign-efi-sig-list -t "$(date +'%Y-%m-%d %H:%M:%S')" -k KEK.key -c KEK.crt db db.esl db.auth

Nos clés sont maintenant prêtes à être installées, mais avant cela nous devons installer le noyau pour que notre système soit en capacité de démarrer.

Mise en place du noyau

La dernière (mais pas la moindre) chose qu'il manque à notre système, c'est un noyau.

Notre noyau sera installé depuis le paquet debian ordinaire, mais celà ne suffit pas : le noyau est installé directement dans /boot, qui dans notre installation se trouve dans le volume chiffré. Il ne sera donc pas accessible lors du démarrage et ne pourra donc pas être chargé. Nous allons donc mettre en place un système permettant, à partir du noyau packagé par Debian, d'avoir un fichier amorçable par l'UEFI.

Création d'une image de noyau unifiée (UKI) amorçable

Pour permettre le démarrage du système, nous allons regrouper tout ce qui est nécessaire, c'est à dire principalement le noyau Linux et son système de fichier initial (initrd), en un seul fichier directement amorçable par l'UEFI, qui sera installé sur la partition EFI. Ce type de fichier est connu sous le nom d'UKI, pour Unified Kernel Image ou, en français, image de noyau unifiée. Pour plus de détails sur ce format, on pourra consulté, par exemple, le wiki Arch. Ce fichier sera signé avec notre clé SecureBoot mise en place à l'étape précédente, ce qui lui permettra d'être chargée directement, sans passer par un chargeur d'amorçage.

On commence par installer les paquets dont nous aurons besoin pour cette étape :

apt-get install systemd-ukify sbsigntool efibootmgr systemd-boot-efi cryptsetup-initramfs

Pour que cette génération se fasse automatiquement à chaque mise à jour du noyau, nous allons la mettre en place dans un script placé dans le répertoire /etc/kernel/postinst.d/ qui sera donc exécuté après chaque installation d'un noyau. Nous allons donc créer un fichier /etc/kernel/postinst.d/zz-1-update-uki (le préfixe permet de contrôler l'ordre d'exécution, dans le cas où d'autres scripts seraient présents) avec le contenu suivant :

#!/bin/sh -e

version="$1"

/usr/lib/systemd/ukify build \
    --linux /boot/vmlinuz-${version} \
    --initrd /boot/initrd.img-${version} \
    --os-release @/usr/lib/os-release \
    --uname ${version} \
    --cmdline @/etc/cmdline \
    --output /boot/efi/EFI/debian/Linux-${version}.efi

/usr/bin/sbsign \
    --key /root/Rocannon-SecureBoot.key \
    --cert /root/Rocannon-SecureBoot.crt \
    --output /boot/efi/EFI/debian/Linux-${version}.efi \
    /boot/efi/EFI/debian/Linux-${version}.efi

Ce script fait référence à plusieurs fichiers externes qui doivent donc être présents et dont les chemins sont potentiellement à adapter :

  • /usr/lib/os-release est installé par le paquet base-files, qui fait partie de l'installation de base,
  • /etc/cmdline est un fichier que nous allons créer et qui nous permet de configurer la ligne de commande de démarrage du noyau. Il se compose de la ligne suivante (à adapter, en particulier selon le nom du volume group choisi lors du partitionnement) :
    root=/dev/mapper/vg_rocannon-lv_racine ro rootflags=subvol=racine quiet loglevel=3 panic=0
    
  • /root/Rocannon-SecureBoot.key et /root/Rocannon-SecureBoot.crt correspondent respectivement à notre clé privée de type db, générée à l'étape précédente, et à son certificat de clé publique.

Notre UKI ne contient pas seulement le noyau, mais également l'initrd associé. Elle doit donc également être régénérée en cas de mise à jour de l'initrd. Pour cela, il nous suffit de créer un lien symbolique vers notre script dans /etc/initramfs/post-update.d/ :

mkdir -p /etc/initramfs/post-update.d
ln -s /etc/kernel/postinst.d/zz-1-update-uki /etc/initramfs/post-update.d/

Installation de l'UKI dans l'UEFI

Une fois notre image créée et signée dans la partition EFI, il faut encore créer une entrée correspondante dans la séquence de démarrage de l'UEFI. Cela se fait avec un second script, également dans /etc/kernel/postinst.d/, que nous allons nommer zz-2-install-uki, pour garantir qu'il s'exécute bien après le précédent :

#!/bin/sh -e

version="$1"

/usr/bin/efibootmgr \
    --disk /dev/nvme0n1 \
    --create \
    --label "Debian-Linux-${version}" \
    --loader "\EFI\debian\Linux-${version}.efi"

Contrairement au précédent, ce script n'a besoin d'être exécuté que lors de la première installation de chaque version du noyau, il ne faut donc pas créer de lien dans /etc/initramfs/post-update.d/.

Gestion de la désinstallation

Pour éviter de saturer notre partition EFI, il faut également supprimer les fichiers que nous y avons créés lors de la désinstallation du noyau correspondant, et mettre à jour la configuration UEFI en conséquence. De façon similaire à l'installation, nous allons mettre en place un script dans le répertoire /etc/kernel/postrm.d/, nommé zz-remove-uki :

#!/bin/sh -e

version="$1"

# recherche du numéro de l'entrée concernée dans l'ordre de démarrage UEFI
num=$(efibootmgr --disk /dev/nvme0n1 \
    | grep "Debian-Linux-${version}" \
    | cut -c 5-8)

if [ -n "$num" ]
then
    efibootmgr \
        --disk /dev/nvme0n1 \
        --delete-bootnum \
        --bootnum $num
fi

# l'option -f évite une erreur si le fichier n'existe pas
rm -f /boot/efi/EFI/debian/Linux-${version}.efi

Installation du noyau

Maintenant que nos scripts sont bien en place, nous pouvons installer le meta-paquet correspondent à l'architecture utilisée :

apt-get install linux-image-amd64
Les paquets linux-image-* suggèrent des paquets correspondant aux divers bootloaders, dont nous n'avons pas besoin. Vérifiez bien qu'apt est configuré pour ne pas installer automatiquement les suggestions.

La sortie de la commande d'installation, et l'examen du contenu de la partition EFI, permettent de vérifier que tout s'est bien passé.

Installation des clés et déchiffrement par le TPM

Tout est maintenant en place, et nous allons pouvoir installer nos clés dans l'UEFI, puis démarrer sur notre système nouvellement installé.

Installation des clés

La mise en place initiale des clés se fait dans l'interface UEFI, il faut donc placer les clés à installer (les trois fichiers .auth généré précédemment) dans un emplacement accessible depuis l'UEFI. Le plus simple est en général de les copier à la racine de la partition EFI. Dans certains cas il est également possible d'utiliser un support amovible comme une clé USB. Il faut ensuite redémarrer la machine, et entrer dans le menu de configuration EFI. Cela peut se faire soit en appuyant sur une touche au démarrage (souvent Suppr ou F2, parfois F1 ou d'autre encore), soit directement depuis un système déjà démarré en utilisant la commande suivante :

systemctl reboot --firmware

Le procédé exact d'installation des clés, et en particulier les libellés utilisés dans les menus, varient selon les constructeurs. Mais la procédure générale est toujours la même : il faut d'abord supprimer la Platform Key existante, pour pouvoir ensuite installer nos clés. Un système sans PK est dit en setup mode, l'option à utiliser dans le menu UEFI peut donc être nommée de deux manières : soit elle parle de setup mode (ou quelque chose d'approchant), soit elle parle de supprimer la clé de plateforme (ou parfois l'ensemble des clés). Selon que l'UEFI offre seulement une option de suppression de l'ensemble des clés, ou des options séparées par type de clés, il peut être nécessaire d'effacer aussi les KEK et db avant d'ajouter les vôtres, en particulier si vous ne souhaitez pas conserver l'ensemble des clés préinstallées.

Une fois la PK supprimée, on peut installer nos clés. Il existe en général un menu qui permet de les importer depuis la partition EFI. Dès que nous aurons installé notre propre PK, le système repassera en mode de fonctionnement ordinaire, et l'installation des autres clés (KEK et db) pourrait, dans certains cas, ne plus être possible (même si, en principe, nos clés devraient être acceptées, puisqu'elles sont signées). Il est donc plus prudent d'installer la PK en dernier.

Le système devrait maintenant être en capacité de démarrer. Toutefois, la clé de déchiffrement du disque n'est pas encore enrollée dans le TPM, le déchiffrement va donc nécessiter la saisie de la phrase de passe LUKS.

Configuration du déchiffrement par le TPM au démarrage

Pour ne plus avoir à saisir la phrase de passe à chaque démarrage, nous allons enregistrer une seconde clé LUKS qui sera stockée dans le TPM. Le TPM possède plusieurs registres nommés PCR, pour Platform Configuration Registers, et numérotés de 0 à 23. Les valeurs contenues dans ces registres sont des hash sha1 et/ou sha256 de différents paramètres mesurés au démarrage comme le code exécutable de l'UEFI ou l'état secure-boot. La clé LUKS sera stockée dans le TPM sous une forme chiffrée par certains de ces registres, et ne pourra ainsi être restituée que si les registres PCR choisis ont les mêmes valeurs que lors de son enregistrement. L'utilisation de chacun de ces registres (pour certains cas l'usage est spécifique à Linux) peut être consulté sur le wiki d'Arch.

Nous utiliserons ici les PCR 0, 2 et 7. D'autres combinaisons sont possibles, par exemple en incluant les n°1 et 3, on verrouille non seulement la version de l'UEFI, mais aussi sa configuration. L'essentiel est de bien inclure le n°7 qui correspond à l'état secure-boot, ce qui inclus non seulement la liste des clés (PK, KEK et db) installées, mais également la clé particulière utilisée pour signer l'OS démarré. Puisque nous avons signé notre noyau avec notre propre clé privée, qui n'est partagée avec personne d'autre, nous avons donc la garantie que notre partition LUKS ne pourra être déchiffrée sans phrase de passe que lors du démarrage de notre UKI, signé par notre clé. En cas de démarrage, par exemple, depuis une image live publiée par une distribution, donc via shim, signé par la clé de Microsoft, la valeur du PCR n°7 sera différente, et le TPM ne fournira pas la clé LUKS. Les opérations qui suivent ne doivent donc être réalisées qu'après avoir réussi à démarrer sur l'UKI signée par notre clé secure-boot, sans quoi les valeurs des PCR utilisées ne seraient pas les bonnes.

Création de la clé et enregistrement dans le TPM

Pour commencer, nous allons créer une nouvelle clé que nous allons stocker dans un premier temps dans un fichier. Elle est convertie en notation hexadecimal car l'outil que nous utiliserons ensuite pour l'enregistrer dans le TPM n'accepte pas les données binaires. Cette clé doit rester secrète, on restreint donc immédiatement les droits au seul propriétaire :

dd if=/dev/random bs=64 count=1 | xxd -p -c64 | tr -d '\n' > /root/cle-luks
chmod 0400 /root/cle-luks

Nous ajoutons ensuite notre nouvelle clé à notre partition LUKS (cette commande demandera la saisie de la phrase de passe existante) :

cryptsetup luksAddKey --pbkdf-force-iterations 4 --pbkdf-memory 32 /dev/nvme0n1p2 /root/cle-luks

Les paramètres --pbkdf-* concernent la fonction dérivation de clé : une phrase de passe n'est souvent pas assez longue pour servir directement de clé, LUKS utilise donc un algorithme qui permet de calculer, à partir de la phrase de passe, une clé suffisemment longue. Cette fonction est paramètrable, pour pouvoir ajuster le bon compromis entre sécurité et rapidité, en fonction des capacités du matériel. Ici nous avons créé une clé dans un fichier, avec une taille suffisante. On peut donc configurer la fonction de dérivation avec une complexité minimale, pour gagner en rapidité de déchiffrement.

On enregistre maintenant cette nouvelle clé dans le TPM avec la commande tpm2-initramfs-tool, fournie par le paquet du même nom. C'est là que nous spécifions les numéros des registres PCR à utiliser :

tpm2-initramfs-tool seal --data $(cat /root/cle-luks) --pcrs 0,2,7

Obtention de la clé pour déchiffrement au démarrage

La clé étant enregistré dans le TPM, nous allons pouvoir la récupérer pour déchiffrer le disque sans saisir la phrase de passe lors du démarrage. Cette récupération est susceptible d'échouer, par exemple en cas de mise à jour de l'UEFI, induisant un changement de valeur d'un des PCR ; il faut donc, en cas d'échec, que le système demande la phrase de passe, pour permettre de démarrer le système et de ré-enregistrer la clé avec les nouvelles valeurs, avec la commande ci-dessus. Ce processus devra se dérouler à l'exécution de l'UKI chargée depuis la partition EFI, nous allons donc placer tout ce qui est nécessaire dans l'initrd, qui est inclus dans l'UKI.

On commence par créer le script suivant, dans le fichier /etc/initramfs-tools/tpm2-cryptsetup :

#!/bin/sh

[ "$CRYPTTAB_TRIED" -lt "1" ] && exec tpm2-initramfs-tool unseal --pcrs 0,2,7

/usr/bin/askpass "Phrase de passe pour $CRYPTTAB_SOURCE ($CRYPTTAB_NAME) : "

Et on le rend exécutable :

chmod +x /etc/initramfs-tools/tpm2-cryptsetup

Il faut maintenant faire en sorte que ce script, ainsi que les exécutables dont il a besoin, soient inclus dans l'initrd. Nous allons pour cela créer un hook dans /etc/initramfs-tools/hooks/tpm2-initramfs-tool :

#!/bin/sh
PREREQ=""
prereqs()
{
    echo "$PREREQ"
}

case $1 in
prereqs)
    prereqs
    exit 0
    ;;
esac

. /usr/share/initramfs-tools/hook-functions

copy_exec /usr/lib/x86_64-linux-gnu/libtss2-tcti-device.so.0
copy_exec /usr/bin/tpm2-initramfs-tool
copy_exec /usr/lib/cryptsetup/askpass /usr/bin
copy_exec /etc/initramfs-tools/tpm2-cryptsetup
copy_exec /usr/bin/sha256sum

Les cinq dernières lignes copient les fichiers nécessaires. Tout ce qui précède est un entête nécessaire aux hooks initramfs, comme cela est indiqué dans la page de manuel d'initramfs-tools (section Header). Ce script doit lui aussi être rendu exécutable :

chmod +x /etc/initramfs-tools/hooks/tpm2-initramfs-tool

Il nous reste à mettre à jour le fichier /etc/crypttab pour indiquer le script à utiliser pour le déchiffrement (le seul changement par rapport à la version précédente est l'ajout de l'option keyscript en fin de ligne) :

# <target name>     <source device>     <key file>  <options>
nvme0n1p2_crypt     /dev/nvme0n1p2      none        luks,discard,keyscript=/etc/initramfs-tools/tpm2-cryptsetup

Vérification du système de fichier racine

Ce type d'installation est potentiellement vulnérable à une attaque, décrite dans cet article (en anglais), qui consiste à remplacer le volume LUKS par un volume similaire, contrôlé par l'attaquant et contenant un processus d'init malveillant qui pourra alors obtenir la clé depuis le TPM, puisqu'aucun PCR n'aura été altéré. Pour s'en prémunir, il faut s'assurer que le système de fichier déchiffré est bien le nôtre, avant de transférer le contrôle au processus d'init qu'il contient.

La solution présentée ici est basée sur ma propre compréhension de l'article cité ci-dessus, et elle est assez différente de ce qui y est suggéré (une solution qui authentifie la partition chiffrée à partir de sa clé, via le PRC 15 du TPM, et dont la mise en œuvre est basée sur l'utilisation de systemd dans l'initrd, ce qui n'est pas le cas ici).

N'ayant pris connaissance de ce problème qu'à la toute fin de la rédaction de ce guide, j'ai opté pour une solution qui changerait le moins possible ce qui était déjà rédigé (et déjà en place sur ma propre installation). Il s'agit donc d'une proposition personnelle, sans aucune garantie.

Pour vérifier l'intégrité du système de fichier, nous allons utiliser une somme de contrôle d'un fichier secret, présent uniquement dans notre volume chiffré, et stable dans le temps. La clé privée secure-boot spécifique à notre machine que nous avons déjà mise en place correspond à ces critères, c'est donc elle que nous allons vérifier avec un script écrit sur le modèle suivant :

#!/bin/sh
PREREQ=""
prereqs()
{
    echo "$PREREQ"
}

case $1 in
prereqs)
    prereqs
    exit 0
    ;;
esac

. /scripts/functions

attendu='<valeur de la somme de contrôle>'
mesure=$(echo "<une valeur aléatoire de salage>" \
    | cat - $rootmnt/chemin/vers/la/clé/privée.key \
    | sha256sum \
    | cut -d ' ' -f 1 \
    | tr -d '\n')

if [ "$mesure" != "$attendu" ]
then
    panic "Système de fichier racine invalide"
fi

Les valeurs entre < et > sont bien sûr à adapter, ainsi que le chemin vers la clé privée. Le salage permet d'éviter une attaque par collision sur la somme de contrôle, par exemple au moyen de rainbow tables, cf. l'article Wikipédia pour plus de détails. La valeur attendu est à déterminée en utilisant la commande de mesure, une fois la valeur de salage fixée.

Attention, la fonction panic arrête le processus de démarrage. En cas d'erreur dans le script, on risque donc de rendre le système impossible à démarrer. Il faudra alors démarrer sur un support live et déchiffrer manuellement le disque (en saississant la phrase de passe) pour tout débloquer. Il est donc plus prudent de remplacer, dans un premier temps, l'appel à panic par un simple affichage, et de ne le rétablir qu'une fois qu'on s'est assuré que tout fonctionne bien.

Ce script est à placer dans le répertoire /etc/initramfs-tools/scripts/local-bottom/ (en le rendant exécutable, comme d'habitude) ce qui permet son exécution après le montage du système de fichier racine, mais avant l'exécution du processus d'init qui s'y trouve.

Il ne reste plus qu'à forcer la regénération de l'initrd :

update-initramfs -u -k all

Au redémarrage, la partition LUKS devrait désormais être déchiffrée automatiquement, sans avoir à saisir la phrase de passe.