Featured image of post Fedora Secure Boot + UKI + TPM2 + LUKS

Fedora Secure Boot + UKI + TPM2 + LUKS

Mise en place d'une boot chain propre avec Secure Boot, clés perso, UKI mesurée et déverrouillage LUKS via TPM2 + PIN

Préambule

Dans cet article je vais montrer comment setup une boot chain assez propre sur Fedora avec :

  • Secure Boot activé
  • mes propres clés Secure Boot avec sbctl
  • les clés Microsoft gardées pour éviter de casser la compatibilité
  • un UKI booté directement
  • une UKI mesurée dans le TPM
  • un disque LUKS unlock via TPM2 + PIN
  • une policy PCR11 signée pour éviter de devoir refaire le setup à chaque update kernel

En gros, si quelqu’un modifie l’UKI, change la cmdline, boot un autre kernel, ou casse la policy attendue, le TPM ne donne pas le secret et on retombe sur la passphrase/recovery key LUKS.

Pour la définition de certains termes c’est ici

Contexte

⚠️
Petit disclaimer avant de continuer.

Dans ce setup, je garde les clés privées Secure Boot et la clé privée utilisée pour signer la PCR policy directement sur la machine. C’est pratique pour automatiser les mises à jour, reconstruire les UKI et signer les nouvelles mesures PCR11, mais ce n’est pas le modèle le plus propre niveau sécurité.

Dans l’idéal, les clés privées ne devraient pas rester stockées sur le PC qu’elles servent à protéger. Si quelqu’un compromet la machine avec assez de privilèges, il pourrait potentiellement récupérer ces clés ou les utiliser pour signer des binaires EFI / UKI, ou encore signer une nouvelle policy PCR autorisant un état de boot qu’on ne voulait pas autoriser.

Une approche plus propre serait de stocker ces clés de signature sur un support matériel externe, par exemple une YubiKey ou un autre support matériel sécurisé compatible avec la signature cryptographique. L’objectif est que la clé privée ne quitte jamais ce support, et qu’elle soit utilisée uniquement au moment de signer une nouvelle UKI ou une nouvelle PCR policy.

Il serait aussi possible d’aller plus loin en supprimant les clés Microsoft de la base Secure Boot, afin que seuls les binaires signés avec mes propres clés soient autorisés à booter. Je ne l’ai pas fait ici pour garder une compatibilité plus simple avec certains binaires EFI, mais c’est une option intéressante pour durcir encore la chaîne de boot.

Pour l’instant, je garde volontairement une approche plus simple et plus facile à reproduire, parce que le but de cet article est d’abord de comprendre et mettre en place la chaîne Secure Boot + UKI + TPM2 + LUKS.

Si je me procure une YubiKey plus tard, je ferai sûrement un autre tuto dédié pour expliquer comment déplacer les clés de signature dessus et rendre le setup plus propre niveau OPSEC.

Avant de rentrer dans les commandes, il faut comprendre pourquoi faire ce setup.

Le but ici ce n’est pas juste d’activer Secure Boot pour avoir un joli SecureBoot enabled dans le terminal. Le vrai sujet, c’est la protection des données personnelles.

Le problème classique, c’est le scénario “evil maid” : quelqu’un peut avoir accès à ton PC pendant que tu n’es pas là, modifier le bootloader, modifier l’initramfs, changer la ligne de commande kernel, ou préparer un boot piégé pour capturer ton mot de passe LUKS au prochain démarrage.

Donc l’idée de ce setup, c’est d’avoir une chaîne de boot plus propre :

  • Secure Boot vérifie que le binaire EFI lancé est bien signé.
  • L’UKI permet d’avoir le kernel, l’initramfs et la cmdline dans un seul fichier EFI signé.
  • Le TPM mesure l’état du boot dans des PCR.
  • LUKS ne se déverrouille via le TPM que si l’état mesuré correspond à ce qu’on attend.
  • Et on ajoute quand même un PIN TPM pour éviter qu’un simple boot automatique suffise.

En gros, je veux que le disque ne se déverrouille pas juste parce que “c’est mon PC”. Je veux qu’il se déverrouille seulement si la machine boot dans un état connu, avec un firmware attendu, Secure Boot actif, une UKI signée, et une mesure TPM valide.

Ce n’est pas une sécurité magique. Si l’OS est déjà compromis pendant qu’il tourne, ce setup ne va pas sauver la session. Si quelqu’un connaît ta passphrase LUKS, pareil. Par contre, ça renforce énormément la partie “avant boot” et ça rend les attaques physiques beaucoup plus compliquées.

Requirements

J’étais sur Fedora avec :

1
2
3
4
5
6
7
mokutil
tpm2-tools
sbsigntools
openssl
sbctl
systemd-ukify
dracut

Installation des outils :

1
2
sudo dnf install mokutil tpm2-tools sbsigntools openssl
sudo dnf install systemd-ukify systemd-boot-unsigned dracut

Pour sbctl, sur mon Fedora le paquet n’était pas dispo directement donc j’ai utilisé un COPR :

1
2
sudo dnf copr enable chenxiaolong/sbctl
sudo dnf install sbctl

État de départ

Déjà je check l’état de Secure Boot :

1
2
3
4
mokutil --sb-state
sudo sbctl status
lsblk -f
findmnt /boot /boot/efi /efi

Chez moi, au début, j’étais en setup mode :

1
2
SecureBoot disabled
Platform is in Setup Mode

Donc parfait, on peut enroll nos propres clés.

Mon disque était déjà en LUKS :

1
2
nvme0n1p3 crypto_LUKS
└─ luks-xxxx btrfs /

Setup secure boot avec ses propres clés

On crée les clés Secure Boot :

1
sudo sbctl create-keys

Ce qui donne :

1
2
3
4
/var/lib/sbctl/keys
├── db
├── KEK
└── PK

Ensuite j’enroll mes clés + les clés Microsoft :

1
sudo sbctl enroll-keys --microsoft

Pourquoi garder Microsoft ?

Parce que sinon tu peux te retrouver à casser certains bootloaders, certaines options firmware, Windows Boot Manager, certains firmwares ou périphériques qui s’attendent encore aux clés Microsoft.

Après reboot :

1
2
3
mokutil --sb-state
sudo sbctl status
cat /sys/kernel/security/lockdown

Résultat attendu :

1
2
3
4
SecureBoot enabled
Setup Mode: Disabled
Secure Boot: Enabled
Vendor Keys: microsoft

Le kernel passe aussi en lockdown mode :

1
none [integrity] confidentiality

Ici [integrity] veut dire que le mode lockdown actif est integrity.

Signer les EFI Fedora

Avant de passer à l’UKI, j’ai signé les binaires EFI classiques :

1
2
3
4
5
6
sudo sbctl sign -s /boot/efi/EFI/fedora/shimx64.efi
sudo sbctl sign -s /boot/efi/EFI/fedora/grubx64.efi
sudo sbctl sign -s /boot/efi/EFI/fedora/mmx64.efi
sudo sbctl sign -s /boot/efi/EFI/fedora/gcdx64.efi
sudo sbctl sign -s /boot/efi/EFI/BOOT/BOOTX64.EFI
sudo sbctl sign -s /boot/efi/EFI/BOOT/fbx64.efi

Puis :

1
sudo sbctl verify

On peut voir les fichiers signés :

1
2
3
✓ /boot/efi/EFI/fedora/shimx64.efi is signed
✓ /boot/efi/EFI/fedora/grubx64.efi is signed
✓ /boot/efi/EFI/BOOT/BOOTX64.EFI is signed

Il restait aussi des fichiers IA32, mais vu que ma machine boot en x64 je m’en fous un peu.

Création de l’UKI

Maintenant on passe au truc intéressant : l’UKI.

Une UKI, c’est une Unified Kernel Image. En gros au lieu d’avoir :

1
shim -> grub -> kernel + initramfs + cmdline

on va avoir un fichier .efi qui contient directement :

1
systemd-stub + kernel + initramfs + cmdline + os-release + signatures PCR

Donc le firmware boot directement un fichier EFI Linux.

Config de base :

1
2
3
4
5
6
7
sudo tee /etc/kernel/uki.conf >/dev/null <<'EOF'
[UKI]
OSRelease=@/etc/os-release
SecureBootPrivateKey=/var/lib/sbctl/keys/db/db.key
SecureBootCertificate=/var/lib/sbctl/keys/db/db.pem
PCRBanks=sha256
EOF

Puis config de kernel-install :

1
2
3
4
5
6
sudo tee /etc/kernel/install.conf >/dev/null <<'EOF'
BOOT_ROOT=/boot/efi
layout=uki
uki_generator=ukify
initrd_generator=dracut
EOF

On génère l’UKI pour le kernel actuel :

1
sudo kernel-install -v add "$(uname -r)" "/lib/modules/$(uname -r)/vmlinuz"

On check :

1
sudo find /boot/efi/EFI/Linux -type f -name "*.efi" -print

Chez moi ça a donné :

1
/boot/efi/EFI/Linux/2355afaf4cf84361ae6a3596ee42a80f-6.19.14-300.fc44.x86_64.efi

On vérifie la signature Secure Boot :

1
2
UKI="$(sudo find /boot/efi/EFI/Linux -type f -name "*.efi" | head -n1)"
sudo sbverify --cert /var/lib/sbctl/keys/db/db.pem "$UKI"

Résultat attendu :

1
Signature verification OK

Entry EFI pour booter l’UKI

J’ai ensuite créé une entrée EFI directe :

1
2
3
4
5
6
7
UKI="/boot/efi/EFI/Linux/2355afaf4cf84361ae6a3596ee42a80f-6.19.14-300.fc44.x86_64.efi"

sudo efibootmgr -c \
  -d /dev/nvme0n1 \
  -p 1 \
  -L "Fedora UKI 6.19.14" \
  -l "\\EFI\\Linux\\$(basename "$UKI")"

Puis check :

1
sudo efibootmgr -v | grep -A3 -Ei 'Fedora UKI|Fedora|Linux'

Après reboot sur cette entrée :

1
2
3
cat /proc/cmdline
sudo tpm2_pcrread sha256:7,11,12
sudo bootctl status

Là le point important c’est :

1
2
3
Measured UKI: yes
Current Stub: systemd-stub
Stub: /EFI/Linux/...

Donc on boot bien sur l’UKI et systemd-stub mesure l’UKI dans le TPM.

Avoir une entrée stable linux.efi

Le problème avec les UKI versionnées, c’est que le nom change à chaque kernel update.

Donc j’ai fait un hook kernel-install qui copie toujours la dernière UKI vers :

1
/boot/efi/EFI/Linux/linux.efi

Comme ça mon entrée EFI reste stable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
sudo tee /etc/kernel/install.d/99-copy-latest-uki.install >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

COMMAND="${1:-}"
KERNEL_VERSION="${2:-}"
BOOT_ROOT="${BOOT_ROOT:-/boot/efi}"
MACHINE_ID="$(cat /etc/machine-id)"
UKI_DIR="$BOOT_ROOT/EFI/Linux"
SRC="$UKI_DIR/${MACHINE_ID}-${KERNEL_VERSION}.efi"
DST="$UKI_DIR/linux.efi"

case "$COMMAND" in
  add)
    if [[ -f "$SRC" ]]; then
      cp -f "$SRC" "$DST"
      echo "Copied latest UKI to $DST"
    else
      echo "UKI not found: $SRC" >&2
      exit 1
    fi
    ;;
  remove)
    # Ne fait rien au remove pour éviter de supprimer le fallback stable.
    ;;
esac
EOF

sudo chmod +x /etc/kernel/install.d/99-copy-latest-uki.install

Puis :

1
sudo kernel-install -v add "$(uname -r)" "/lib/modules/$(uname -r)/vmlinuz"

On vérifie :

1
2
sudo ls -lh /boot/efi/EFI/Linux/
sudo sbverify --cert /var/lib/sbctl/keys/db/db.pem /boot/efi/EFI/Linux/linux.efi

Ensuite je crée une entrée EFI stable :

1
2
3
4
5
sudo efibootmgr -c \
  -d /dev/nvme0n1 \
  -p 1 \
  -L "Fedora UKI stable" \
  -l "\\EFI\\Linux\\linux.efi"

Puis je mets seulement cette entrée en boot order :

1
sudo efibootmgr -o 0004

À adapter avec ton ID EFI évidemment.

LUKS + TPM2

Au début j’ai tenté un truc simple :

1
2
3
4
sudo systemd-cryptenroll /dev/nvme0n1p3 \
  --tpm2-device=auto \
  --tpm2-with-pin=yes \
  --tpm2-pcrs=7+11

Sauf que ce n’est pas pratique.

Pourquoi ?

Parce que PCR11 change dès que l’UKI change. Donc à chaque update kernel, ton TPM ne déverrouille plus LUKS.

C’est logique mais chiant.

Ce qu’on veut, c’est :

1
2
PCR7 direct
PCR11 via signature

En gros :

  • PCR7 vérifie l’état Secure Boot / clés firmware
  • PCR11 vérifie l’UKI, mais via une policy signée, donc les updates kernel restent possibles

PCR signature pour éviter de casser à chaque update

Pour ça, il faut une clé de signature PCR.

J’ai d’abord essayé RSA 4096. Mauvaise idée dans mon cas : mon TPM n’a pas aimé.

Ensuite j’ai essayé EC prime256v1.

Ça avait l’air plus propre, mais au boot j’avais :

1
Esys_VerifySignature() Esys Finish ErrorCode (0x000002d2)

En décodant :

1
tpm2_rc_decode 0x000002d2

Résultat :

1
tpm:parameter(2):unsupported or incompatible scheme

Donc mon TPM annonçait bien ecdsa, mais il refusait le scheme exact utilisé ici par systemd/TPM pour VerifySignature.

Finalement la solution qui marche chez moi : RSA 2048.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
sudo rm -f /etc/systemd/tpm2-pcr-private-key-initrd.pem
sudo rm -f /etc/systemd/tpm2-pcr-public-key-initrd.pem

sudo openssl genpkey -algorithm RSA \
  -pkeyopt rsa_keygen_bits:2048 \
  -out /etc/systemd/tpm2-pcr-private-key-initrd.pem

sudo openssl rsa \
  -in /etc/systemd/tpm2-pcr-private-key-initrd.pem \
  -pubout \
  -out /etc/systemd/tpm2-pcr-public-key-initrd.pem

sudo chmod 600 /etc/systemd/tpm2-pcr-private-key-initrd.pem
sudo chmod 644 /etc/systemd/tpm2-pcr-public-key-initrd.pem

Vérification :

1
2
3
4
sudo openssl rsa \
  -in /etc/systemd/tpm2-pcr-private-key-initrd.pem \
  -text -noout \
  | grep 'Private-Key'

Résultat :

1
Private-Key: (2048 bit, 2 primes)

Ajout de la PCR signature dans uki.conf

Dans /etc/kernel/uki.conf, j’ai ajouté :

1
2
3
[PCRSignature:initrd]
PCRPrivateKey=/etc/systemd/tpm2-pcr-private-key-initrd.pem
PCRPublicKey=/etc/systemd/tpm2-pcr-public-key-initrd.pem

Chez moi le fichier final ressemble à ça :

1
2
3
4
5
6
7
8
9
[UKI]
OSRelease=@/etc/os-release
SecureBootPrivateKey=/var/lib/sbctl/keys/db/db.key
SecureBootCertificate=/var/lib/sbctl/keys/db/db.pem
PCRBanks=sha256

[PCRSignature:initrd]
PCRPrivateKey=/etc/systemd/tpm2-pcr-private-key-initrd.pem
PCRPublicKey=/etc/systemd/tpm2-pcr-public-key-initrd.pem

On régénère :

1
sudo kernel-install -v add "$(uname -r)" "/lib/modules/$(uname -r)/vmlinuz"

On check l’UKI :

1
2
sudo ukify inspect /boot/efi/EFI/Linux/linux.efi \
  | grep -Ei '\.pcrpkey|\.pcrsig|pcrs|pkfp|pol|sig' -A10 -B4

On doit voir :

1
2
3
.pcrpkey
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...

Enroll LUKS avec TPM2 + PIN + PCR7 + PCR11 signée

Ajout d’une recovery key :

1
sudo systemd-cryptenroll /dev/nvme0n1p3 --recovery-key

Ensuite on wipe l’ancien token TPM :

1
sudo systemd-cryptenroll /dev/nvme0n1p3 --wipe-slot=tpm2

Et on enroll le nouveau :

1
2
3
4
5
6
sudo systemd-cryptenroll /dev/nvme0n1p3 \
  --tpm2-device=auto \
  --tpm2-with-pin=yes \
  --tpm2-pcrs=7 \
  --tpm2-public-key=/etc/systemd/tpm2-pcr-public-key-initrd.pem \
  --tpm2-public-key-pcrs=11

Ce que ça veut dire :

1
--tpm2-pcrs=7

On bind directement sur PCR7, donc l’état Secure Boot / clés firmware.

1
--tpm2-public-key-pcrs=11

On bind PCR11 via une policy signée. Donc PCR11 peut changer avec les updates kernel, tant que la nouvelle UKI contient une signature valide générée par notre clé PCR.

Vérification :

1
2
3
4
sudo systemd-cryptenroll /dev/nvme0n1p3

sudo cryptsetup luksDump /dev/nvme0n1p3 \
  | sed -n '/Tokens:/,/Digests:/p'

On veut voir :

1
2
3
4
5
systemd-tpm2
tpm2-hash-pcrs: 7
tpm2-pubkey-pcrs: 11
tpm2-pin: true
tpm2-primary-alg: rsa

Résultat final

Après reboot, j’ai :

1
sudo bootctl status

avec :

1
2
3
4
5
Secure Boot: enabled
TPM2 Support: yes
Measured UKI: yes
Current Stub: systemd-stub
Stub: /EFI/Linux/linux.efi

Et au boot :

1
2
PIN TPM demandé
pas de passphrase LUKS ensuite

Donc le flow final est :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Firmware UEFI
Secure Boot vérifie la signature de /EFI/Linux/linux.efi
systemd-stub lance l’UKI
systemd-stub mesure les sections de l’UKI dans PCR11 :
kernel, initrd, os-release, cmdline intégrée, etc.
systemd-cryptsetup demande le PIN TPM
TPM vérifie :
  - PCR7 direct
  - PCR11 via signature .pcrsig + clé publique PCR
  - PIN TPM
si tout matche : TPM libère le secret LUKS
LUKS unlock
Fedora boot

Et si quelqu’un modifie linux.efi ?

Cas 1 : modification bête du fichier.

1
2
3
4
5
linux.efi modifié
signature Secure Boot cassée
UEFI refuse de booter

Cas 2 : attaquant remplace par une autre UKI signée par une clé autorisée Secure Boot.

1
2
3
4
5
6
7
Secure Boot accepte potentiellement
PCR11 devient différent
pas de .pcrsig valide avec ta clé PCR
TPM refuse de libérer la clé LUKS

Cas 3 : attaquant a aussi ta clé PCR privée.

1
il peut signer une nouvelle policy PCR11

Là, il manque encore le PIN TPM, mais oui, la clé PCR privée devient très sensible.

Est-ce que les updates kernel vont casser le setup ?

Normalement non.

C’est justement le but de la PCR11 signée.

À chaque update kernel :

  1. kernel-install génère une nouvelle UKI
  2. ukify signe l’UKI Secure Boot
  3. ukify ajoute la .pcrsig
  4. mon hook copie la dernière UKI vers /boot/efi/EFI/Linux/linux.efi
  5. l’entrée EFI stable continue de booter linux.efi

Donc je n’ai pas besoin de refaire systemd-cryptenroll à chaque update kernel.

Par contre, si je change :

  • les clés Secure Boot
  • la clé PCR /etc/systemd/tpm2-pcr-*.pem
  • la config /etc/kernel/uki.conf
  • la cmdline kernel
  • l’état Secure Boot dans le BIOS
  • le TPM est reset
  • ou certaines options firmware qui changent PCR7

là oui, je peux devoir rentrer la passphrase/recovery et refaire le token TPM.

Update : rebuild automatique de l’UKI après certaines updates

En fait, kernel-install est bien appelé automatiquement quand le paquet kernel-core est installé ou supprimé.

On peut le voir avec :

1
rpm -q --scripts kernel-core kernel-modules kernel-modules-core

Dans mon cas, kernel-core contient bien un truc du genre :

1
/bin/kernel-install add 6.19.14-300.fc44.x86_64 /lib/modules/6.19.14-300.fc44.x86_64/vmlinuz

Ça c’est OK.

Mais il y a un cas un peu plus relou : les updates de modules.

Par exemple kernel-modules ne relance pas forcément kernel-install de base. Il peut juste faire :

1
2
depmod -a <version>
dracut -f --kver <version> /boot/initramfs-<version>.img

Sauf que moi je ne boot pas vraiment sur /boot/initramfs-*.img.

Je boot sur :

1
/boot/efi/EFI/Linux/linux.efi

Donc si Fedora régénère seulement l’initramfs classique, mais pas l’UKI, mon fichier réellement booté peut rester ancien.

L’idée n’est pas de hooker dracut directement.

Mauvaise idée :

1
2
3
4
5
6
7
dracut
kernel-install
dracut
boucle / comportement sale

Le point d’entrée propre reste kernel-install.

Donc j’ai créé un petit script qui rebuild l’UKI du kernel courant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
sudo tee /usr/local/sbin/rebuild-current-uki >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

LOCK="/run/rebuild-current-uki.lock"
exec 9>"$LOCK"
flock -n 9 || exit 0

log() {
  echo "[rebuild-current-uki] $*"
}

KVER="${1:-$(uname -r)}"
VMLINUZ="/lib/modules/${KVER}/vmlinuz"

if [[ ! -f "$VMLINUZ" ]]; then
  log "vmlinuz not found for $KVER: $VMLINUZ"
  exit 0
fi

log "rebuilding UKI for $KVER"

kernel-install -v add "$KVER" "$VMLINUZ"

UKI="/boot/efi/EFI/Linux/linux.efi"

if [[ -f "$UKI" ]]; then
  log "UKI updated:"
  ls -lh "$UKI"

  if command -v ukify >/dev/null 2>&1; then
    log "PCR sections:"
    ukify inspect "$UKI" | grep -Ei '\.pcrsig|\.pcrpkey|pcr|signature' -A8 -B4 || true
  fi

  if command -v sbverify >/dev/null 2>&1 && [[ -f /var/lib/sbctl/keys/db/db.pem ]]; then
    log "Secure Boot signature check:"
    sbverify --cert /var/lib/sbctl/keys/db/db.pem "$UKI" || true
  fi
else
  log "warning: expected UKI not found at $UKI"
fi
EOF

sudo chmod +x /usr/local/sbin/rebuild-current-uki

Le flock sert juste à éviter que le script parte plusieurs fois en parallèle si plusieurs hooks se déclenchent dans la même transaction.

Ensuite j’installe le plugin DNF5 actions :

1
sudo dnf install libdnf5-plugin-actions

Puis j’ajoute une action post-transaction DNF5 :

1
2
3
4
5
6
7
sudo tee /etc/dnf/libdnf5-plugins/actions.d/99-rebuild-uki.actions >/dev/null <<'EOF'
post_transaction:kernel-modules*:in:enabled=1:/usr/local/sbin/rebuild-current-uki
post_transaction:kernel-modules-core*:in:enabled=1:/usr/local/sbin/rebuild-current-uki
post_transaction:akmod-*:in:enabled=1:/usr/local/sbin/rebuild-current-uki
post_transaction:kmod-*:in:enabled=1:/usr/local/sbin/rebuild-current-uki
post_transaction:zfs*:in:enabled=1:/usr/local/sbin/rebuild-current-uki
EOF

Pourquoi je n’ai pas mis kernel-core* ?

Parce que kernel-core appelle déjà kernel-install via les scriptlets RPM Fedora.

Là je cible surtout les cas où des modules bougent :

1
2
3
4
5
kernel-modules*
kernel-modules-core*
akmod-*
kmod-*
zfs*

C’est peut-être un peu bourrin, mais je préfère ça plutôt qu’avoir un initramfs classique à jour et une UKI bootée qui ne l’est pas.

Pour tester, j’ai fait :

1
sudo dnf reinstall "kernel-modules-$(uname -r)"

D’abord le scriptlet Fedora de kernel-modules fait son truc :

1
Running: dracut -f --kver 6.19.14-300.fc44.x86_64 /boot/initramfs-6.19.14-300.fc44.x86_64.img

Puis mon action DNF déclenche le rebuild UKI :

1
2
3
4
5
6
7
8
9
BOOT_ROOT (/boot/efi) set via config.
layout=uki set via config
INITRD_GENERATOR (dracut) set via config.
UKI_GENERATOR (ukify) set via config.

Using plugins:
  /usr/lib/kernel/install.d/50-dracut.install
  /usr/lib/kernel/install.d/60-ukify.install
  /etc/kernel/install.d/99-copy-latest-uki.install

Maintenant le flow est plus propre :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
update kernel-core
scriptlet Fedora
kernel-install add
UKI rebuild

update kernel-modules / akmod / kmod / zfs
DNF5 post_transaction
rebuild-current-uki
kernel-install add "$(uname -r)"
UKI rebuild

Il y a un petit défaut : dans certains cas ça peut faire deux dracut.

Par exemple avec kernel-modules, Fedora peut déjà faire :

1
dracut -f /boot/initramfs-...

puis mon hook fait :

1
2
3
4
5
kernel-install add
dracut dans le staging kernel-install
ukify

Ce n’est pas très grave. C’est juste un peu plus long.

Le point important pour moi, c’est que le fichier réellement booté reste cohérent :

1
/boot/efi/EFI/Linux/linux.efi

Et que ce fichier contient toujours une .pcrsig valide pour que le TPM puisse unlock LUKS après update.

Commandes utiles de vérification

Secure Boot :

1
2
3
mokutil --sb-state
sudo sbctl status
sudo sbctl verify

UKI :

1
2
3
sudo bootctl status
sudo sbverify --cert /var/lib/sbctl/keys/db/db.pem /boot/efi/EFI/Linux/linux.efi
sudo ukify inspect /boot/efi/EFI/Linux/linux.efi

TPM PCR :

1
sudo tpm2_pcrread sha256:7,11,12,15

LUKS token :

1
2
sudo systemd-cryptenroll /dev/nvme0n1p3
sudo cryptsetup luksDump /dev/nvme0n1p3 | sed -n '/Tokens:/,/Digests:/p'

Test manuel du token TPM :

1
2
3
4
5
6
sudo cryptsetup open \
  --test-passphrase \
  --token-only \
  --token-id 1 \
  --debug \
  /dev/nvme0n1p3

Les fichiers à backup

Très important :

1
2
sudo tar -C /var/lib/sbctl -czf ~/sbctl-keys-backup.tar.gz keys
sudo chown "$USER:$USER" ~/sbctl-keys-backup.tar.gz

À backup hors de la machine :

1
2
3
4
5
/var/lib/sbctl/keys/
/etc/systemd/tpm2-pcr-private-key-initrd.pem
/etc/systemd/tpm2-pcr-public-key-initrd.pem
/etc/kernel/uki.conf
/etc/kernel/cmdline

Et évidemment garder la recovery key LUKS.

Définitions

Secure Boot

Secure Boot est une fonctionnalité UEFI qui permet au firmware de vérifier la signature des binaires lancés au boot. En gros, le firmware ne lance pas n’importe quel fichier .efi : il vérifie d’abord si ce fichier est signé par une clé de confiance.

UKI

UKI veut dire Unified Kernel Image.

Normalement, sur beaucoup de setups Linux, le boot ressemble à un truc du genre :

1
2
3
4
5
firmware UEFI
  -> shim / GRUB / systemd-boot
  -> kernel
  -> initramfs
  -> cmdline kernel

Avec une UKI, on regroupe tout dans un seul fichier EFI :

1
systemd-stub + kernel + initramfs + cmdline + os-release + signatures PCR

Donc au lieu d’avoir plein de morceaux séparés, on a un seul fichier .efi bootable, signé, et mesurable par le TPM. C’est plus propre pour Secure Boot, parce que la ligne de commande kernel et l’initramfs font partie de l’image signée.

TPM2

Le TPM, pour Trusted Platform Module, est une puce de sécurité présente sur beaucoup de machines modernes. Elle peut stocker ou protéger des secrets, mais surtout elle peut les libérer seulement si certaines conditions sont respectées.

Dans ce setup, le TPM ne contient pas “la passphrase LUKS en clair”. Il protège un secret utilisé pour déverrouiller LUKS, et ce secret peut être lié à l’état de la machine au boot.

Donc si l’état attendu change, par exemple Secure Boot désactivé, UKI modifiée, mauvais kernel, mauvaise cmdline, etc., le TPM peut refuser de libérer le secret.

PCR

PCR veut dire Platform Configuration Register.

Ce sont des registres du TPM qui contiennent des mesures de l’état de la machine. Ce ne sont pas des fichiers qu’on édite à la main. Ce sont plutôt des valeurs calculées progressivement pendant le boot.

L’idée importante : si un élément mesuré change, la valeur du PCR change aussi.

Dans mon setup, les deux PCR importants sont surtout :

1
2
PCR7  -> état Secure Boot / clés / politique firmware
PCR11 -> mesures liées à l’UKI avec systemd-stub

PCR7 permet de lier le déverrouillage à l’état Secure Boot. PCR11 permet de lier le déverrouillage à l’UKI bootée.

PCR signature

Le problème, c’est que PCR11 change quand l’UKI change. Et comme l’UKI change à chaque update kernel, un enroll TPM naïf sur PCR11 casserait le déverrouillage LUKS à chaque mise à jour.

La solution propre, c’est d’utiliser une PCR policy signée.

Au lieu de dire au TPM :

1
déverrouille seulement si PCR11 vaut exactement cette valeur

on lui dit plutôt :

1
déverrouille si la valeur PCR11 correspond à une mesure signée par ma clé de confiance

Comme ça, on peut mettre à jour son kernel, générer une nouvelle UKI, signer les nouvelles mesures PCR, et garder un système maintenable.

Conclusion

Au final le setup marche bien, mais le plus relou c’était clairement TPM2 + PCR signature.

Le piège principal :

1
2
EC P-256 avait l'air propre
mais mon TPM refusait le scheme de signature

Donc dans mon cas :

1
2
3
RSA 4096 → pas bon
EC P-256 → pas bon
RSA 2048 → OK

Maintenant j’ai une boot chain clean :

1
2
3
4
5
6
7
8
Secure Boot custom keys + Microsoft
UKI signée
Measured UKI
TPM2 + PIN
LUKS unlock automatique (après avoir entré le PIN)
PCR7 direct
PCR11 signée
updates kernel OK

Et c’est exactement ce que je voulais.

Built with Hugo
Theme Stack designed by Jimmy