Démarrer un kernel Linux sur un Squashfs

01 avril 2023 ― Pierre-Loup GOSSE

Résumé ― La vérification de l'intégrité des fichiers via leur somme de contrôle (checksum) est un critère important dans les systèmes sécurisés. Cette vérification permet d'assurer au démarrage que les fichiers n'ont pas été altérés par des tiers malveillants. Lorsque cette vérification concerne un système de fichiers dans son ensemble la vérification individuelle des fichiers rend l'opération complexe. Nous allons voir dans cette article comment simplifier cette verification en utilisant un Squashfs comme système de fichiers racine. Sans aborder la vérification en elle-même, nous détaillerons les étapes pour démarrer un kernel Linux sur un Squashfs ainsi que ses limites et comment y palier avec la mise en place d'un Overlayfs.

Système de fichiers Squashfs

Le Squashfs est un système de fichiers compressé en lecture seule qui se présente sous le forme d'un fichier. La somme de contrôle du fichier Squashfs permet a elle seule la vérification de l'intégrité de l'ensemble des fichiers du système de fichiers. De plus, son accès exclusif en lecture assure la non modification des fichiers.

L'outil mksquashfs permet de créer un Squashfs à partir d'un dossier source :

$ mkdir boot-squashfs
$ cd boot-squashfs
$ mkdir rootfs
$ echo "Bonjour !" > rootfs/bonjour.txt
$ mksquashfs rootfs/ rootfs.squashfs
$ file rootfs.squashfs

rootfs.squashfs: Squashfs filesystem, little endian, version 1024.0, compressed [...]

Le fichier Squashfs rootfs.squashfs contient un système de fichiers. Étant un lui-même un fichier, il est contenu dans un système de fichiers.

Montage d'un Squashfs en loop device

L'accès au système de fichiers contenu dans un Squashfs nécessite un étape intermédiare. Pour rappel, un système de fichiers est l'organisation des fichiers au sein d'un volume physique ou logique. Un volume physique, tel qu'un périphérique de stockage, peut se diviser en partition pour faire cohabiter plusieurs systèmes de fichiers. Sous Linux les périphériques et leurs partitions sont accessibles par bloc de mémoire via des block devices présent dans /dev. Pour accéder aux fichiers présents dans un système de fichiers il nécessaire de monter le block device associé à sa partition dans un répertoire appelé le point de montage. À partir de ce point de montage le kernel Linux est capable de réaliser des lectures / écritures sur les fichiers.

Le Squashfs étant un fichier, le montage de son système de fichiers doit alors passer par un élément intermédiaire : le loop device. Un loop device est un pseudo-périphérique qui permet de rendre accessible un fichier comme un block device. En d'autres termes, cela permet de manipuler le fichier comme un périphérique physique. Les loop devices sont représentés par les pseudo-périphérique /dev/loop* sous Linux. Le binaire losetup permet l'association de fichier à un loop device. Pour associer un loop device à notre fichier rootfs.squashfs il faut d'abord connaître un pseudo-périphérique non utilisé :

$ losetup -f

/dev/loop0

# losetup /dev/loop0 rootfs.squashfs

note
Pour utiliser les loop devices il est nécessaire que le module kernel loop.ko soit inséré. Ce dernier est souvent compilé avec l'image kernel Linux, la commande suivante permet de le vérifier :

$ grep 'loop' /lib/modules/$(uname -r)/modules.builtin

kernel/drivers/block/loop.ko

Sinon il est nécessaire de l'insérer manuellement avec la commande modprobe loop.

Vous pouvez alors observer avec lsblk la présence du loop device :

$ lsblk

NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
loop0         7:15   0     4K  0 loop
...

Le fichier étant maintenant accessible par bloc de mémoire, nous pouvons le monter dans un dossier via la commande mount :

$ mkdir mnt
# mount /dev/loop0 mnt
$ lsblk

NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
loop0         7:15   0     4K  0 loop ~/boot-squashfs/mnt
...

$ cat mnt/bonjour.txt

Bonjour !

Pour dissocier le loop device au fichier Squashfs il faut utiliser l'option -d ou --detach :

# losetup -d /dev/loop0

La dissociation ne démonte cependant pas le dossier, il faut le faire manuellement (ou d'abord démonter puis dissocier) :

$ lsblk

NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
loop0         7:15   0     4K  0 loop ~/boot-squashfs/mnt
...

# umount mnt
$ lsblk

NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
...

L'association d'un loop device pour un Squashfs peut être réalisé directement par la commande mount avec les options suivantes :

# mount -t squashfs -o loop rootfs.squashfs mnt

note
Le type du système de fichiers, spécifié avec l'option -t, peut être détecté par mount impliquant automatiquement l'option -o loop pour les Squashfs. Ainsi, la commande mount rootfs.squashfs mnt est aussi valable.

Création d'un rootfs

Un Squashfs peut contenir n'importe quel type de fichier. Nous souhaitons dans cet article l'utiliser commme le système de fichiers racine, dit rootfs, de notre kernel Linux. Il est ainsi nécessaire que le rootfs contiennent un certains nombre de fichier essentiel tel que le processus init. Par simplicité nous allons construire un rootfs basé sur la distribution Debian pour l'architecture x86 64-bits à partir de l'outil debootstrap :

# debootstrap --arch=amd64 --variant=minbase --include=systemd buster debian

note
L'option include permet l'installation de paquet Debian durant la génération du rootfs. Par praticité installons le système d'initialisation (i.e. le processus init) systemd sur notre rootfs.

Nous avons à présent un rootfs minimal que nous utiliserons pour notre Squashfs. Vous pouvez configurer celui-ci à votre convenance. Par exemple, nous pouvons définir son hostname :

# echo "boot-squashfs" > debian/etc/hostname

Nous allons aussi ajouter un utilisateur admin avec les droits root (uid 0 et gid 0) sans mot de passe en exécutant la commande useradd et passwd via chroot :

# chroot debian useradd -ou 0 -g 0 -m -s /bin/bash admin
# chroot debian passwd -d admin

Démarrage via un initramfs

Durant sa phase d'initialisation, le kernel monte le premier système de fichiers qu'il trouve comme son rootfs et tente d'exécuter son processus init. En cas d'échec du montage, le kernel passe en "kernel panic : Unable to mount root fs" et s'arrête. Le rootfs à monter peut être spécifié via le paramètre kernel root=<value>. La valeur <value> permet d'identifier le périphérique où se trouve le système de fichiers, tel que root=PARTUUID=<uuid>, root=PARTLABEL=<label> ou directement le block device associé avec root=/dev/sda1 par exemple. Cette analyse du paramètre root= s'observe dans le code source du kernel dans le fichier init/do_mounts.c avec les fonctions root_dev_setup et name_to_dev_t. Il convient alors de constater qu'il n'est pas possible d'indiquer au kernel de monter notre fichier Squashfs comme le rootfs avec le paramètre root. En effet, notre Squashfs étant un fichier il est stocké dans un système de fichiers dit parent, il n'est donc pas accessible par le kernel au démarrage. Nous pouvons alors décider de monter dans un premier temps le système de fichiers parent via root=, puis une fois le kernel démarré exécuter le montage de notre Squashfs. Cependant, dans ce cas de figure c'est le système de fichiers parent qui sera le rootfs et non celui contenu dans notre Squashfs. Ainsi, pour monter notre Squashfs comme le rootfs il est nécessaire de réaliser des opérations en amont par l'intermédiare d'un initramfs. Un initramfs est un système de fichiers temporaire chargé au démarrage en mémoire vive par le kernel Linux pour préparer le montage du rootfs et y basculer son exécution en exécutant son processus init.

note
Les termes initrd et initramfs font référence au même usage, seul leur forme diffère. L'initrd est un système de fichiers rendu accessible par le kernel en mémoire vive via le block device /dev/ram0 puis monté tel le rootfs temporaire. L'initramfs est une archive cpio décompressée par le kernel dans un système de fichiers temporaire tmpfs en mémoire vive. Le terme "initrd" reste souvent utilisé pour faire référence à l'usage d'un rootfs d'initialisation en mémoire vive bien qu'il peut s'agir en réalité d'une archive cpio.

Outil initramfs-tools

L'utilisation d'un initramfs étant trés courant dans la plus part des systèmes, des outils permettent d'en générer facilement. Nous utiliserons l'outil initramfs-tools notamment utilisé sous les distributions Debian. Initramfs-tools permet de générer un initramfs pour le rootfs et son kernel Linux sur lequel il s'exécute. En fonction des fichiers présents dans le rootfs il détermine les opérations que doit réaliser l'initramfs pour préparer le rootfs avant d'y basculer l'exécution du kernel. Par exemple, si l'utilisateur configure un outil de chiffrement de disque sur son rootfs il sera nécessaire que l'initramfs effectue le déchiffrement pour rendre accessible le rootfs au kernel. En regénérant un initramfs, initramfs-tools prendra en compte les opérations de déchiffrement à réaliser.

Nous pouvons installer le paquet initramfs-tools via APT dans notre rootfs Debian :

# chroot debian/ apt-get install initramfs-tools

Plutôt que d'analyser l'intégralité du rootfs à la recherche d'élément permettant de déterminer les opérations, initramfs-tools possède deux répertoires de configuration : /etc/initramfs-tools et /usr/share/initramfs-tools. Par convention, la configuration présente dans /etc/ est statique, celle dans /usr/share peut être ajoutée et modifiée. Ainsi, les outils nécessitant des opérations durant l'exécution de l'initramfs vont déposer des fichiers dans le dossier /usr/share/initramfs-tools et imposer la regénération de l'initramfs via initramfs-tools.

Le dossier /usr/share/initramfs-tools se décompose en :

  • Un script shell init qui sera le processus init de l'initramfs.
  • Un dossier conf.d pour inclure des variables shell au processus init par fichier individuel.
  • Un dossier modules.d pour ajouter et insérer au démarrage des modules kernel par fichier individuel.
  • Un dossier hooks pour ajouter des hooks scripts shell exécutés durant la génération de l'initramfs.
  • Un dossier scripts pour ajouter des boots scripts shell exécutés par le processus init de l'initramfs.

Le processus init /usr/share/initramfs-tools/init analyse les arguments de ligne de commande kernel pour définir des variables shell puis exécute de manière ordonné des boots scripts avant et après le montage du rootfs dans /root. Enfin, le processus bascule l'exécution du kernel en exécutant le processus init se trouvant dans /root. Le périphérique du rootfs à monter est déterminé via l'argument root= de la ligne de commande. Vous pouvez l'observer dans la fonction local_mount_root du fichier /usr/share/initramfs-tools/scripts/local qui se charge de monter le rootfs dans le dossier /root :

~/boot-squashfs/debian/usr/share/initramfs-tools/scripts/local

local_mount_root()
{
    # ...

    # Mount root
    if ! mount ${roflag} ${FSTYPE:+-t "${FSTYPE}"} ${ROOTFLAGS} "${ROOT}" "${rootmnt?}"; then
        panic "Failed to mount ${ROOT} as root file system."
    fi
}

La variable $ROOT correspond à la valeur de l'argument root= et $rootmnt à la valeur /root.

Configuration

Le démarrage sur un Squashfs nécessite l'ajout d'opérations dans l'initramfs, à savoir le montage du système de fichiers parent du Squashfs puis son association au loop device défini par l'argument root= de la ligne de commande. Ces opérations peuvent être réalisées en ajoutant un boot script dans le répertoire de configuration /usr/share/initramfs-tools/scripts d'initramfs-tools. Vous remarquerez que le dossier est décomposé en plusieurs sous-dossiers, chacun d'eux correspond à une étape dans le fil d'exécution du processus init avant et après le montage du rootfs.

Boot scripts

À partir des indications de la documentation de Debian nous allons ajouter notre boot script dans le dossier local-premount pour s'exécuter juste avant le montage du rootfs.

Notre boot script doit commencer par une entête permettant à initramfs-tools de gérer un ordre d'exécution entre les boots scripts :

~/boot-squashfs/debian/usr/share/initramfs-tools/scripts/local-premount/squashfs

#!/bin/sh

PREREQ=""

prereqs()
{
    echo "$PREREQ"
}

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

# Fonctions initramfs-tools
. /scripts/functions

# Variable $ROOT définie par le processus init, correspondant à l'argument
# root= de la ligne de commande kernel
log_begin_msg "Attach Squashfs to ${ROOT}"

La première opération consiste à monter la partition du système de fichiers parent. Il faut alors déterminer son block device associé. L'attribution des block devices pouvant varier en fonction du partitionnement et du peuplement du dossier /dev/ par le kernel, il est préférable de se baser sur les nommages persistants disponibles dans /dev/disk. Les fichiers étant des liens symboliques, la commande realpath permet de récupérer le block device associé. Nous choisirons d'identifier la partition par son label via le dossier dev/disk/by-partlabel. Pour rendre le script générique, nous attendrons un argument squashfs.partlabel= dans la ligne de commande kernel ― accessible via le pseudo-fichier /proc/cmdline ― pour spécifier le label de la partition contenant le fichier Squashfs :

~/boot-squashfs/debian/usr/share/initramfs-tools/scripts/local-premount/squashfs

#!/bin/sh

# ...

# Récupère l'argument squashfs.partlabel pour idenfitier la partition
partlabel=$(cat /proc/cmdline | sed -nr -e 's/.*squashfs\.partlabel=([^= ]+).*/\1/p')
if [ -z "$partlabel" ] ; then
    panic "Squashfs partlabel not specified. Boot arguments must include a squashfs.partlabel= parameter"
fi

# Récupère le device associé à la partition
dev="$(realpath "/dev/disk/by-partlabel/${partlabel}")"
if [ $? -ne 0 ] ; then
    panic "Partlabel '${partlabel}' not found"
fi

# Montage de la partition
mkdir /mnt
mount $dev /mnt -t ext4

note
La solution proposée ici se veut générique. Il est tout à fait possible de déterminer la partition via un autre nommage persistant tel que l'UUID par exemple ou bien d'écrire en dur le nom de la partition à monter.

La seconde opération est d'associer le fichier Squashfs se trouvant dans /mnt au loop device défini par l'argument root= accessible par la variable $ROOT. Dans le même principe que l'identification de la partition à monter, nous pouvons attendre un argument squashfs.file= pour déterminer le chemin d'accès du fichier Squashfs dans le dossier /mnt.

~/boot-squashfs/debian/usr/share/initramfs-tools/scripts/local-premount/squashfs

#!/bin/sh

# ...

# Récupère l'argument squashfs.file pour le nom du fichier Squashfs
file=$(cat /proc/cmdline | sed -nr -e 's/.*squashfs\.file=([^= ]+).*/\1/p')
if [ -z "$file" ] ; then
    panic "Squashfs file not specified. Boot arguments must include a squashfs.file= parameter"
fi

if [ ! -f "/mnt/${file}" ] ; then
    panic "Could not find the Squashfs file ${file}"
fi

# Associe le fichier Squashfs au loop device passé en argument root
if losetup $ROOT /mnt/${file} ; then
    log_success_msg "Squashfs attached to ${ROOT} with success"
else
    panic "Could not attach the Squashfs to ${ROOT}"
fi

log_end_msg

exit 0

Voici le fichier ~/boot-squashfs/debian/usr/share/initramfs-tools/scripts/local-premount/squashfs dans son intégralité. Donnez les droits d'exécution à votre boot script via chmod +x.

Modules

Pour fonctionner correctement certains binaires nécessitent l'insertion de modules dans le kernel. C'est le cas pour losetup avec le driver loop mais aussi pour mount avec le driver squashfs pour le montage de Squashfs. Le driver loop est présent par défaut dans l'initramfs mais n'est pas inséré, tandis que le driver squashfs n'est pas présent. Pour ajouter ces modules et les insérer dans l'initramfs il est nécessaire de modifier l'initramfs via initramfs-tools. Deux options s'offrent à nous. La première est la création d'un hook script pour indiquer à la génération de l'initramfs l'insertion au démarrage des deux modules via la fonction force_load du fichier /usr/share/initramfs-tools/hook-functions. La seconde est de créer un fichier dans le dossier /usr/share/initramfs-tools/modules.d et de lister les modules. Nous allons opter pour la seconde solution avec le fichier ~/boot-squashfs/debian/usr/share/initramfs-tools/modules.d/modules :

~/boot-squashfs/debian/usr/share/initramfs-tools/modules.d/modules

loop
squashfs

Génération

La génération d'un initramfs avec initramfs-tools peut se réaliser avec la commande update-initramfs. Cependant, pour générer un initramfs il est nécessaire que le rootfs possède une image kernel Linux. En effet, l'initramfs étant exécuté par un kernel ― en général celui présent sur le rootfs où il a été généré ― l'usage des modules doit correspondre à la même version. Il faut donc installer le paquet linux-image-amd64 dans notre rootfs :

# chroot debian/ apt-get install linux-image-amd64

Vous remarquerez la génération automatique de l'initramfs durant l'installation du paquet. Vous pouvez cependant générer un initramfs manuellement :

# chroot debian update-initramfs -k all -c

Le fichier initramfs /boot/initrd.img suffixé par la version du kernel a été généré.

Extraction

Il peut être nécessaire d'extraire l'initramfs pour valider les modifications apportées. Nous pouvons dédier un dossier temporaire dans /tmp pour en extraire son contenu :

$ mkdir -p /tmp/initramfs
$ cp debian/boot/initrd.img* /tmp/initramfs
$ cd /tmp/initramfs
$ zcat initrd.img* | cpio -idm

167487 blocs

Vous pouvez retrouver notre boot script dans /tmp/initramfs/scripts/local-premount/squashfs. La commande lsinitramfs permet quant à elle de lister les fichiers présent dans l'initramfs :

$ lsinitramfs debian/boot/initrd.img*

Émulation du démarrage sur le Squashfs

Nous allons créer notre Squashfs à partir de notre rootfs Debian. Puis nous émulerons le démarrage du kernel Linux via QEMU avec l'interface UEFI en partant d'une image disque ― représentant le volume physique ― préalablement partitionnée et peuplée avec le Squashfs.

note
Dans le cas d'un machine physique le partitionnement et le peuplage de son volume physique sont généralement des opérations réalisées par un installeur. Pour simplifier notre émulation nous réalisons ces opérations en montant l'image disque avec un loop device depuis la machine hôte.

Image disque

Une image disque de 500Mo sera suffisant pour l'exercice de cet article. Nous créerons deux partitions boot et primary. La première sera une partition EFI avec un système de fichiers FAT qui contiendra notre initramfs et l'image kernel présent dans le dossier /boot de notre rootfs ainsi qu'un bootloader. La seconde sera une partition avec un système de fichiers ext4 contenant le fichier de notre Squashfs.

# dd if=/dev/zero of=disk.img bs=1M count=500
# losetup /dev/loop0 disk.img
# parted -s --align=optimal /dev/loop0 mklabel gpt
# parted -s --align=optimal /dev/loop0 mkpart boot fat32 1M 128M
# parted -s --align=optimal /dev/loop0 set 1 esp on
# parted -s --align=optimal /dev/loop0 mkpart primary 128M 100%
# mkfs.vfat -F 32 /dev/loop0p1
# mkfs.ext4 /dev/loop0p2

Approfondir
L'article Créer une clé bootable pour x86 64-bits rentre dans les détails du processus de démarrage sous x86 64-bits.

Fichier Squashfs

L'initramfs et l'image kernel devant être dans la partition boot, le dossier /boot doit être exclu du Squashfs pour éviter les doublons. L'option -e de mksquashfs permet d'exclure un fichier ou dossier. Nous voulons cependant garder le dossier /boot vide afin d'y monter par la suite la partition boot, ainsi nous n'excluons que son contenu avec un motif en regex :

$ mksquashfs debian rootfs.squashfs -noappend -no-recovery -regex -e '^boot/.*'

Bootloader GRUB

Il est nécessaire d'utiliser un bootloader pour charger en mémoire notre initramfs et notre kernel avant d'exécuter ce dernier. Nous utiliserons le bootloader GRUB.

L'outil grub-mkimage permet de générer un bootloader GRUB. Sous Debian, l'outil est présent dans le paquet grub-common.

$ grub-mkimage -o bootx64.efi -p /efi/boot -O x86_64-efi fat part_gpt normal boot linux configfile loopback chain efifwsetup ls search search_label search_fs_uuid search_fs_file test loadenv exfat ext2

Peuplement des partitions

La dernière étape consiste à monter les partitions de notre image disque et d'y copier nos éléments. Nous allons créer un dossier disk avec deux sous-dossiers boot et primary :

$ mkdir -p disk/{boot,primary}

De la même manière que le Squashfs, associons un loop device à notre image disque afin de monter ses partitions dans les sous-dossiers :

# losetup -P /dev/loop0 disk.img

L'option -P ou --partscan est nécessaire lorsque que le fichier est partitionné pour créer un loop device par partition. Vous pouvez observer avec lsblk deux sous loop device p1 et p2 étant respctivement la partition boot et primary de notre image disque.

$ lsblk

NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
loop0         7:34   0   500M  0 loop
├─loop0p1   259:7    0   121M  0 part
└─loop0p2   259:8    0   377M  0 part
...

Nous pouvons désormais monter ces deux loop device dans nos sous-dossiers :

# mount /dev/loop0p1 disk/boot
# mount /dev/loop0p2 disk/primary

Copions l'ensemble des éléments présent dans le dossier boot de notre rootfs Debian dans le dossier disk/boot et renommons par simplicité l'image kernel et l'initramfs :

# cp -a debian/boot/* disk/boot
# mv disk/boot/vmlinuz* disk/boot/vmlinuz
# mv disk/boot/initrd.img* disk/boot/initrd.img

Reste à ajouter notre bootloader bootx64.efi. Ce dernier doit se situer dans le dossier EFI/Boot de la partition boot afin d'être détecté par l'UEFI. Nous allons l'accompagner d'un fichier de configuration GRUB afin d'automatiser le chargement en mémoire et le démarrage de notre kernel avec nos paramètres :

~/boot-squashfs/grub.cfg

menuentry 'Linux' {
    linux /vmlinuz console=ttyS0 squashfs.file=rootfs.squashfs squashfs.partlabel=primary root=/dev/loop0
    initrd /initrd.img
}

Copions ces deux fichiers dans le dossier disk/boot/EFI/Boot :

# mkdir -p disk/boot/EFI/Boot
# cp bootx64.efi grub.cfg disk/boot/EFI/Boot

rappel
Le système de fichiers FAT32 ne gère les permissions. Linux affichera les fichiers du dossier disk/boot avec la permission 755.

Pour finir, copions le Squashfs dans le sous-dossier disk/primary :

# cp rootfs.squashfs disk/primary

Exécutez la commande sync afin d'assurer que la copie des fichiers est effective sur l'image disque.

Émulation avec QEMU

Pour tester notre solution nous allons émuler l'exécution avec QEMU à partir de notre image disque. QEMU utilise par défaut l'interface BIOS, l'option -bios permet d'utiliser une autre interface tel que l'UEFI. Le projet OVMF, installable via le paquet Debian ovmf, permet d'utiliser l'UEFI sous QEMU en passant à l'option -bios le fichier /usr/share/ovmf/OVMF.fd.

# qemu-system-x86_64 -enable-kvm -m 1G -drive format=raw,file=disk.img -nographic -bios /usr/share/ovmf/OVMF.fd

Le menu GRUB s'affiche, appuyez sur "Entrée". En analysant les traces du kernel nous observons la ligne Unpacking initramfs... qui témoigne de l'utilisation de notre initramfs. Puis plus loin la ligne Run /init as init process qui montre le démarrage du processus init de l'initramfs. Par la suite nous observons alors les logs de notre boot script avec Begin: Attach Squashfs rootfs.squashfs from /dev/sda2 to /dev/loop0 ... et Success: Squashfs attached to /dev/loop0 with success. Une fois le Squashfs monté l'initramfs pivote l'exécution dessus en exécutant le processus init, ici systemd.

Le login apparaît, authentifiez vous avec l'utilisateur admin créé en amont. La commande lsblk affiche les périphériques suivant :

# lsblk

NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
loop0    7:0    0 256.4M  0 loop /
sda      8:0    0   500M  0 disk
├─sda1   8:1    0   121M  0 part
└─sda2   8:2    0   377M  0 part
...

Le périphérique loop0 correspond au loop device de notre Squashfs, monté à la racine par notre initramfs. Le périphérique sda et ses deux partitions correspond à notre image disque. Vous pouvez monter la partition boot dans /boot :

# mount /dev/sda1 /boot

Lecture / écriture avec un Overlayfs

Le Squashfs étant en lecture seule, si vous tentez une écriture ― en créant un fichier par exemple ― vous serez confronté au message suivant :

# touch toto

touch: cannot touch 'toto': Read-only file system

Cela peut être problématique si vous désirez avoir le droit d'écriture tout en gardant les avantages du Squashfs. Nous allons voir dans cette partie comment palier ce problème avec la mise en place d'un Overlayfs.

L'Overlayfs est lui aussi un système de fichiers. Son mécanisme permet de fusionner l'accès à un dossier dont les fichiers sont présents dans deux autres dossiers. On parle alors de lowerdir et upperdir pour distinguer les deux dossiers qui peuvent provenir du même système de fichiers. Les modifications sur le dossier sont appliquées dans le upperdir, laissant intact le lowerdir. Un troisième dossier workdir est alors nécessaire pour rendre les modifications atomiques sur le upperdir, il doit être sur le même système de fichier que ce dernier.

Ainsi, afin d'avoir les droits d'écriture dans notre home il faut monter un overlay dans /home/admin avec en lowerdir le même dossier, de manière à garder les éventuels dossier déjà présent, et en upperdir et workdir deux dossiers présents dans un système de fichiers possédant l'accès en écriture, par exemple un tmpfs.

# mount tmpfs -t tmpfs /tmp
# mkdir /tmp/upper
# mkdir /tmp/work
# mount overlayfs -t overlay -o lowerdir=/home/admin,upperdir=/tmp/upper,workdir=/tmp/work /home/admin

Si vous exécutez la commande mount depuis le dossier destination, ici /home/admin, vous remarquerez que l'erreur d'écriture Read-only file system persiste. En effet, le processus du shell fait toujours référence au /home/admin avant le montage de l'overlay, c'est-à-dire à celui présent sur le Squashfs, en lecture seule. La commande cd, permettant de changer le répertoire du shell, peut alors être utilisée pour rafraîchir le dossier /home/admin et prendre en compte l'overlay.

La commande mount affiche le montage de l'overlay et ses paramètres :

# mount | grep /home/admin

overlayfs on /home/admin type overlay (rw,relatime,lowerdir=/home/admin,upperdir=/tmp/upper,workdir=/tmp/work)

Il est désormais possible d'écrire dans le dossier /home/admin. Vous observerez que vos fichiers sont en réalité écrits dans l'upperdir /tmp/upper. Tel un système de fichiers classique, vous pouvez démonter l'overlay via umount /home/admin. Si vous avez l'erreur target is busy cela signifie probablement que vous essayez de démonter l'overlay tout en étant dans le dossier destination, déplacez-vous alors dans un autre dossier. Le dossier /home/admin redevient vide mais vos fichiers sont toujours présents dans l'upperdir.

Montage dans l'initramfs

Dans le cas où vous souhaitez rendre l'ensemble du rootfs en lecture/écriture il est nécessaire de monter un Overlayfs sur la racine. De la même manière que le montage du dossier /home/admin, nous pourrions envisager le montage suivant :

# mount overlayfs -t overlay -o lowerdir=/,upperdir=/tmp/upper,workdir=/tmp/work /

La commande ne retourne pas d'erreur, cependant vous observerez que l'erreur d'écriture persiste même en changeant de dossier avec cd. Le montage d'un overlay à la racine du rootfs doit s'effectuer en amont, dans l'initramfs, pour s'assurer que tout les processus utilisent la référence du / de l'overlay et non celui du Squashfs.

Nous allons donc ajouter un nouveau boot script pour monter un Overlayfs. Cette nouvelle opération ne peut s'ajouter dans notre boot script actuel. En effet, ce dernier est réalisé à l'étape local-premount (i.e. avant le montage du rootfs) tandis que le montage de l'overlay doit se réaliser après. Nous opterons donc pour ajouter notre boot script overlay à l'étape init-bottom, c'est-à-dire juste avant la bascule sur le rootfs.

À l'exécution du boot script, le Squashfs est monté dans /root, ce dossier est donc en lecture seule. Nous souhaitons monter l'overlay sur le dossier /root avec des dossiers upper et work accessible en lecture / écriture et le dossier lower contenant le Squashfs. Afin de s'assurrer que ces trois dossiers soient accessibles une fois la bascule sur /root, il est préférable de les créer dans /root et non pas dans / qui est le rootfs de l'initramfs. Cependant, le Squashfs étant monté dans /root il est nécessaire de déplacer son point de montage temporairement pour créer les trois dossiers /root/lower, /root/upper et /root/work. Puis, de replacer le Squashfs dans /root/lower cette fois-ci avant de monter l'overlay. Voici le boot script dans son intégralité :

~/boot-squashfs/debian/usr/share/initramfs-tools/scripts/init-bottom/overlay

#!/bin/sh

PREREQ=""

prereqs()
{
    echo "$PREREQ"
}

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

. /scripts/functions

log_begin_msg "Setup the overlay"

# Création du dossier read-only pour déplacer temporairement le Squashfs
mkdir /squashfs
mount -o move /root /squashfs

# Il est désormais possible d'écrire dans /root.
# On y créer un dossier read-only pour le Squashfs
mkdir /root/lower
mount -o move /squashfs /root/lower

# Création des dossiers upper et work
mkdir /root/upper
mkdir /root/work

# Monte un Overlayfs dans /root (le dossier où l'on va basculer) avec en
# lower les fichiers du Squashfs en lecture seule et en upper / work
# des dossiers en lecture / écriture
mount overlayfs -t overlay -o lowerdir=/root/lower,upperdir=/root/upper,workdir=/root/work /root

log_end_msg

exit 0

note
Ne pas oublier de donner les droits d'exécution à votre nouveau boot script.

L'usage de l'Overlayfs nécessite l'insertion du module overlay. De la même manière que les modules loop et squashfs, il est nécessaire d'ajouter ce module à liste déjà existante dans le fichier ~/boot-squashfs/debian/usr/share/initramfs-tools/modules.d/modules :

~/boot-squashfs/debian/usr/share/initramfs-tools/modules.d/modules

loop
squashfs
overlay

Il ne reste plus qu'à regénérer l'initramfs, le copier la partition boot de l'image disque toujours monter et de relancer QEMU :

# chroot debian update-initramfs -k all -c
# cp -a debian/boot/initrd.img* disk/boot/initrd.img
# sync
# qemu-system-x86_64 -enable-kvm -m 1G -drive format=raw,file=disk.img -nographic -bios /usr/share/ovmf/OVMF.fd
# mount | grep overlay

overlayfs on / type overlay (rw,relatime,lowerdir=/root/lower,upperdir=/root/upper,workdir=/root/work)

Les dossiers /root/lower, /root/upper et /root/work ne sont cependant pas accessible en l'état, le dossier /root étant vide. En effet, le dossier /root actuel est celui du Squashfs, tandis que les dossiers lower, upper et work correspondent aux dossiers créés depuis le dossier /root de l'initramfs. Ainsi, les dossiers sont comme cachés mais existent, vous avez désormais les droits d'écriture sur tout le rootfs. Vous aurez cependant observé le message overlayfs: upper fs does not support tmpfile dans les traces du kernel au moment de l'exécution de notre boot script overlay. En effet, l'initramfs étant chargé en mémoire vive, les dossiers lower, upper et work sont donc volatiles : un redémarrage du système provoquera la perte des écritures réalisées.

Persistance des modifications

Pour rendre les écritures persistantes il est nécessaire que le dossier upper provienne d'un système de fichiers non volatile. Nous avons à notre disposition une image disque avec une partition primary, contenant notre fichier Squashfs, avec de l'espace libre. Ainsi, nous pouvons créer les dossiers upper et work sur cette partition.

Nous allons modifier le boot script overlay. Afin de garder la possibilité de démarrer le système sans persistante, nous activons la persistance si l'argument persist est fourni dans la ligne de commande kernel.

~/boot-squashfs/debian/usr/share/initramfs-tools/scripts/init-bottom/overlay

#!/bin/sh

PREREQ=""

prereqs()
{
    echo "$PREREQ"
}

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

. /scripts/functions

persist=$(cat /proc/cmdline | grep -q "persist" && echo "true" || echo "false")

if $persist ; then
    log_begin_msg "Setup a persistent overlay"
else
    log_begin_msg "Setup a volatile overlay"
fi

# Création du dossier read-only pour déplacer temporairement le Squashfs
mkdir /squashfs
mount -o move /root /squashfs

# Il est désormais possible d'écrire dans /root.
# On y créer un dossier read-only pour le Squashfs
mkdir /root/lower
mount -o move /squashfs /root/lower

upper=/root/upper
work=/root/work

if $persist ; then
    # Création des dossiers upper et work persistant dans le point de
    # montage /mnt réalisé dans le boot script squashfs
    upper=/mnt/upper
    work=/mnt/work
fi

mkdir "$upper"
mkdir "$work"

# Monte un Overlayfs dans /root (le dossier où l'on va basculer) avec en
# lower les fichiers du Squashfs en lecture seule et en upper / work
# des dossiers en lecture / écriture
mount overlayfs -t overlay -o lowerdir=/root/lower,upperdir=${upper},workdir=${work} /root

log_end_msg

exit 0

Nous pouvons désormais ajouter une nouvelle entrée dans le menu GRUB grub.cfg pour démarrer avec ou sans persistance :

~/boot-squashfs/grub.cfg

menuentry 'Linux with persistent memory' {
    linux /vmlinuz console=ttyS0 persist squashfs.file=rootfs.squashfs squashfs.partlabel=primary root=/dev/loop0
    initrd /initrd.img
}

menuentry 'Linux with volatile memory' {
    linux /vmlinuz console=ttyS0 squashfs.file=rootfs.squashfs squashfs.partlabel=primary root=/dev/loop0
    initrd /initrd.img
}

Regénérez l'initramfs et mettez-le à jour dans la partition boot ainsi que la configuration GRUB. Notre overlay étant maintenant monté sur la partition primary il est nécessaire de démonter les partitions avant de lancer QEMU afin d'éviter des incohérences sur les opérations d'écritures.

# chroot debian update-initramfs -k all -c
# cp -a debian/boot/initrd.img* disk/boot/initrd.img
# cp grub.cfg disk/boot/EFI/Boot/grub.cfg
# sync
# umount disk/*
# qemu-system-x86_64 -enable-kvm -m 1G -drive format=raw,file=disk.img -nographic -bios /usr/share/ovmf/OVMF.fd
# mount | grep overlay

overlayfs on / type overlay (rw,relatime,lowerdir=/root/lower,upperdir=/mnt/upper,workdir=/mnt/work)

Vous observerez que le dossier /mnt est cependant vide. De la même manière qu'avec les dossiers tmp, l'overlay ayant été monté depuis l'initramfs les chemins /mnt/upper et /mnt/work sont du point de vue de l'initramfs lorsque nous avons monté la partition primary dans le dossier /mnt. Rien n'empêche, tel l'initramfs, de monter cette partition dans /mnt :

# mount /dev/sda2 /mnt
# ls /mnt

lost+found  rootfs.squashfs  upper  work

Les dossiers upper et work deviennent accessible. En analysant le dossier upper vous pouvez déterminer les fichiers qui ont été modifiés depuis le démarrage du système. Notamment dans les dossiers /etc et /var. Essayez d'écrire un fichier dans votre home, puis quittez QEMU. En montant la partition primary vous retrouverez votre fichier :

# echo "hello world !" > toto

# mount /dev/loop0p2 disk/primary
# cat disk/primary/upper/home/admin/toto

hello world !

# umount disk/primary

En exécutant à nouveau QEMU avec la mémoire peristante le fichier sera toujours présent.

Conclusion

L'usage d'un Squashfs peut être un bon choix pour construire un système sécurisé en protégeant et vérifiant l'intégrité du Squashfs. En effet, dans l'état actuel de notre démonstration rien n'empêche un utilisateur de modifier le Squashfs directement depuis la mémoire. Pour contrer cette faille, nous pouvons chiffrer la mémoire pour protéger celle-ci de toute modification sans le mot de passe. Cependant, une fois le système démarré et le Squashfs dechiffré, un utilisateur ayant les droits priviligiés (root) peut décompresser le Squashfs, y appliquer des modifications, le recompresser puis redémarrer le système. Ainsi, sans vérification de l'intégrité du Squashfs au démarrage, par l'initramfs, rien ne permet d'assurer que celui-ci n'a pas été altéré. Le Squashfs étant un fichier, un simple checksum permet de valider son intégrité. Le checksum à comparer peut être calculé en amont et utilisé par l'initramfs pour la vérification. Un système trés sécurisé peut mettre en place une chaîne de confiance avec Trusted Plateform Module combiné avec le Secure Boot de l'UEFI afin de sauvegarder de manière chiffré et sécurisé le checksum utilisé par l'initramfs.

Dans cette article nous n'avons aussi pas abordé la configuration SQUASHFS du kernel Linux. Notamment l'option SQUASHFS 4K DEVBLK SIZE permettant d'améliorer les performances lecture / écriture du Squashfs.

Bibliographie

Squashfs, https://fr.wikipedia.org/wiki/SquashFS
Mksquashfs, https://www.mankier.com/1/mksquashfs
Partition, https://fr.wikipedia.org/wiki/Partition_(informatique)
Block devices, https://en.wikipedia.org/wiki/Device_file#Block_devices
Point de montage, https://fr.wikipedia.org/wiki/Point_de_montage
Loop device, https://en.wikipedia.org/wiki/Loop_device
Losetup, https://linux.die.net/man/8/losetup
Processus init, https://fr.wikipedia.org/wiki/Init
Distribution, https://fr.wikipedia.org/wiki/Distribution_Linux
Debootstrap, https://wiki.debian.org/Debootstrap
Systemd, https://fr.wikipedia.org/wiki/Systemd
Useradd, https://manpages.debian.org/buster/passwd/useradd.8.en.html
Passwd, https://manpages.debian.org/buster/passwd/passwd.1.fr.html
Chroot, https://fr.wikipedia.org/wiki/Chroot
Paramètre kernel, https://www.kernel.org/doc/html/v4.14/admin-guide/kernel-parameters.html
Init/do_mounts.c, https://elixir.bootlin.com/linux/latest/source/init/do_mounts.c#L303
Initramfs, https://fr.wikipedia.org/wiki/Initrd
Cpio, https://fr.wikipedia.org/wiki/Cpio
Tmpfs, https://fr.wikipedia.org/wiki/Tmpfs
Initramfs-tools, https://manpages.debian.org/stretch/initramfs-tools-core/initramfs-tools.8.en.html
Paquet initramfs-tools, https://packages.debian.org/buster/initramfs-tools
Documentation, https://manpages.debian.org/stretch/initramfs-tools-core/initramfs-tools.8.en.html#Subdirectories
Nommages persistants, https://wiki.archlinux.org/title/persistent_block_device_naming
~/boot-squashfs/debian/usr/share/initramfs-tools/scripts/local-premount/squashfs, squashfs
/usr/share/initramfs-tools/hook-functions, https://manpages.debian.org/stretch/initramfs-tools-core/initramfs-tools.8.en.html#Help_functions
Update-initramfs, https://manpages.debian.org/bullseye/initramfs-tools/update-initramfs.8.en.html
Paquet linux-image-amd64, https://packages.debian.org/fr/buster/linux-image-amd64
QEMU, https://fr.wikipedia.org/wiki/QEMU
UEFI, https://fr.wikipedia.org/wiki/UEFI
Installeur, https://www.debian.org/CD/live/
EFI, https://en.wikipedia.org/wiki/EFI_system_partition
FAT, https://fr.wikipedia.org/wiki/File_Allocation_Table
Créer une clé bootable pour x86 64-bits, /articles/usb-bootable-x86-64/
GRUB, https://fr.wikipedia.org/wiki/GNU_GRUB
Grub-common, https://packages.debian.org/bullseye/grub-common
Sync, https://linux.die.net/man/8/sync
OVMF, https://github.com/tianocore/tianocore.github.io/wiki/OVMF
Overlayfs, https://fr.wikipedia.org/wiki/OverlayFS
Tmpfs, https://fr.wikipedia.org/wiki/Tmpfs
Trusted Plateform Module, https://wiki.archlinux.org/title/Trusted_Platform_Module
Secure Boot, https://access.redhat.com/documentation/fr-fr/red_hat_enterprise_linux/7/html/system_administrators_guide/sec-uefi_secure_boot
Configuration SQUASHFS, https://cateee.net/lkddb/web-lkddb/SQUASHFS.html
SQUASHFS 4K DEVBLK SIZE, https://cateee.net/lkddb/web-lkddb/SQUASHFS_4K_DEVBLK_SIZE.html