Aller au contenu
Sécurité medium

Compiler un noyau Linux durci KSPP (Debian et RHEL)

21 min de lecture

Certaines protections du noyau Linux ne s'activent pas au runtime : elles sont figées à la compilation. Pour les obtenir, il faut recompiler le noyau de votre distribution (pas un noyau vanilla) avec le sous-ensemble KSPP (Kernel Self-Protection Project) qui reste boot-safe, l'empaqueter proprement en .deb ou .rpm, puis le prouver. Ce guide donne la recette complète pour Debian 12/13 et Ubuntu, puis pour RHEL/AlmaLinux 8/9, avec les pièges rencontrés à chaque étape.

Ce guide s'adresse à un public avancé déjà à l'aise avec le durcissement système. Le durcissement runtime (sysctl, modules, GRUB, montages) est traité dans des pages dédiées, citées plus bas : ici, on s'attaque au seul durcissement que ces réglages ne peuvent pas couvrir, celui du noyau compilé.

  • Pourquoi un durcissement noyau passe par la recompilation, et quand ne pas le faire
  • Le sous-ensemble KSPP boot-safe à activer, et les symboles renommés selon la version
  • Compiler un noyau durci sous Debian/Ubuntu (bindeb-pkg) et sous RHEL/AlmaLinux (rpmbuild)
  • Ajouter les mitigations spéculatives au boot via la ligne de commande noyau
  • Prouver le résultat avec kernel-hardening-checker, /boot/config et /proc/cmdline

La plupart des réglages de durcissement s'appliquent à chaud : sysctl change un paramètre noyau, modprobe blackliste un module, une option de montage retire des droits. Mais une classe entière de protections KSPP est décidée à la compilation (CONFIG_*) et ne peut pas être basculée après coup. Exemples : le poisoning des pages libérées (PAGE_POISONING), la randomisation de la disposition des structures (RANDSTRUCT), les plugins GCC de durcissement, ou le forçage de signature des modules (MODULE_SIG_FORCE). Pour ces options, la seule remédiation est un noyau compilé avec.

Le principe directeur du durcissement reste le même : on travaille sur la configuration effective, pas sur un fichier d'intention. Pour le noyau en cours d'exécution, cette configuration effective se lit dans /boot/config-$(uname -r), pas dans un .config de build oublié quelque part. On part de ce fichier, on lui applique les options KSPP, on reconstruit, et on relit le /boot/config du noyau installé pour vérifier ce qui a réellement pris.

Deux règles encadrent l'exercice. D'abord, on recompile le noyau de la distribution (ses sources, sa configuration de base), pas un noyau vanilla de kernel.org : on conserve ainsi les correctifs et la compatibilité matérielle du distributeur. Ensuite, on vise un sous-ensemble boot-safe : le KSPP complet inclut des options comme MODULES=n ou CFI_CLANG qui cassent un build GCC de distribution ou un système qui charge des modules. On les écarte pour livrer un noyau qui démarre.

  • Une VM jetable ou une machine de test (jamais la production en direct), avec 12 cœurs, 12 Go de RAM et 40 Go de disque pour un build confortable.
  • Un accès root, la possibilité de redémarrer, et un snapshot si votre stockage le permet.
  • Les dépôts sources activés : deb-src sous Debian/Ubuntu, le dépôt *-source du noyau sous RHEL/AlmaLinux.
  • De quoi récupérer l'ancien noyau au boot (une entrée GRUB de secours), au cas où.

La force de la liste ci-dessous est d'être cross-version : scripts/config ignore silencieusement un symbole absent du noyau ciblé, donc la même liste sert de la 4.18 (RHEL 8) aux noyaux 6.x (Debian 13, AlmaLinux 10, Ubuntu 26). Les symboles renommés au fil des versions portent les deux orthographes, seule celle présente est prise en compte :

ProtectionAncien symboleNouveau symbole
Randomisation des structuresGCC_PLUGIN_RANDSTRUCT (≤ 4.x)RANDSTRUCT_FULL (≥ 5.x)
Initialisation de la pileGCC_PLUGIN_STRUCTLEAK_BYREF_ALLINIT_STACK_ALL_ZERO (≥ 5.x)
Isolation des tables de pages, retpolinePAGE_TABLE_ISOLATION, RETPOLINEpréfixe MITIGATION_* (≥ 6.8)

Les options à activer couvrent la détection de corruption (BUG_ON_DATA_CORRUPTION, DEBUG_LIST, DEBUG_SG), la protection mémoire (PAGE_POISONING, HARDENED_USERCOPY, SLAB_FREELIST_HARDENED, SLAB_FREELIST_RANDOM, INIT_ON_ALLOC_DEFAULT_ON, INIT_ON_FREE_DEFAULT_ON), le durcissement du contrôle de flux et de la pile (STACKPROTECTOR_STRONG, VMAP_STACK, SCHED_STACK_END_CHECK, RANDOMIZE_BASE), les plugins GCC (GCC_PLUGIN_LATENT_ENTROPY, RANDSTRUCT, STACKLEAK), la signature forcée des modules (MODULE_SIG_FORCE, MODULE_SIG_ALL) et les restrictions d'exposition (SECURITY_DMESG_RESTRICT, SECURITY_YAMA, STRICT_KERNEL_RWX).

Les options à désactiver retirent des surfaces d'attaque devenues inutiles sur un serveur : HIBERNATION, KEXEC, IA32_EMULATION, PROC_KCORE, MODIFY_LDT_SYSCALL, LEGACY_PTYS, BINFMT_MISC, ACPI_CUSTOM_METHOD, DEVKMEM, SLAB_MERGE_DEFAULT, X86_VSYSCALL_EMULATION.

Sous Debian et Ubuntu, la cible bindeb-pkg produit directement un paquet .deb propre. On récupère les sources du noyau de la distribution, on applique les options avec scripts/config, on vérifie que le pare-feu a survécu, puis on construit.

  1. Installer les dépendances de build

    Fenêtre de terminal
    sudo apt-get update
    sudo apt-get install -y build-essential fakeroot dpkg-dev debhelper \
    libncurses-dev bison flex libssl-dev libelf-dev bc dwarves rsync kmod \
    cpio lz4 zstd lzop xz-utils
    GCCV=$(gcc -dumpversion | cut -d. -f1)
    sudo apt-get install -y "gcc-${GCCV}-plugin-dev"

    Le paquet gcc-*-plugin-dev fournit les plugins GCC (latent_entropy, stackleak), et debhelper est indispensable sur Debian 13/trixie (compat 13), sinon le build échoue en une vingtaine de secondes.

  2. Récupérer les sources qui correspondent au noyau courant

    Fenêtre de terminal
    cd /usr/src
    SRCVER=$(dpkg-query -W -f='${source:Version}' "linux-image-$(uname -r)")
    sudo apt-get source "linux=$SRCVER"
    cd "$(find /usr/src -maxdepth 1 -type d -name 'linux-*' | sort | tail -1)"
    cp "/boot/config-$(uname -r)" .config

    On part du .config du noyau en cours, c'est-à-dire de la configuration effective, pas d'un defconfig générique.

  3. Appliquer les options KSPP

    Fenêtre de terminal
    KSPP_ENABLE="BUG_ON_DATA_CORRUPTION DEBUG_LIST DEBUG_NOTIFIERS DEBUG_SG DEBUG_WX \
    FORTIFY_SOURCE HARDENED_USERCOPY PANIC_ON_OOPS PAGE_POISONING RANDOMIZE_BASE \
    RANDOMIZE_MEMORY SCHED_STACK_END_CHECK SECCOMP SECCOMP_FILTER SECURITY_DMESG_RESTRICT \
    SECURITY_YAMA SLAB_FREELIST_HARDENED SLAB_FREELIST_RANDOM STACKPROTECTOR_STRONG \
    STRICT_KERNEL_RWX STRICT_MODULE_RWX SYN_COOKIES VMAP_STACK MODULE_SIG MODULE_SIG_ALL \
    MODULE_SIG_FORCE GCC_PLUGINS GCC_PLUGIN_LATENT_ENTROPY GCC_PLUGIN_RANDSTRUCT \
    RANDSTRUCT_FULL GCC_PLUGIN_STACKLEAK INIT_ON_ALLOC_DEFAULT_ON INIT_ON_FREE_DEFAULT_ON"
    KSPP_DISABLE="ACPI_CUSTOM_METHOD BINFMT_MISC COMPAT_BRK DEVKMEM HIBERNATION \
    IA32_EMULATION KEXEC LEGACY_PTYS MODIFY_LDT_SYSCALL PROC_KCORE SLAB_MERGE_DEFAULT \
    X86_VSYSCALL_EMULATION"
    NF_STACK="NETFILTER NF_CONNTRACK NF_TABLES NF_TABLES_INET NFT_CT"
    for o in $KSPP_ENABLE $NF_STACK; do scripts/config --enable "CONFIG_$o"; done
    for o in $KSPP_DISABLE; do scripts/config --disable "CONFIG_$o"; done
    scripts/config --disable SYSTEM_TRUSTED_KEYS --disable SYSTEM_REVOCATION_KEYS
    make olddefconfig

    On vide SYSTEM_TRUSTED_KEYS et SYSTEM_REVOCATION_KEYS : les chemins de certificats Debian n'existent pas dans un rebuild, et un certificat absent casse le build. En revanche, ne videz pas MODULE_SIG_KEY : sa valeur par défaut certs/signing_key.pem est auto-générée, et un MODULE_SIG_FORCE=y avec clé vide fait échouer sign-file.

  4. Vérifier que le pare-feu a survécu, puis construire

    Fenêtre de terminal
    grep -qE '^CONFIG_NF_TABLES=[ym]' .config || { echo "ERREUR: nftables absent, build annulé"; exit 1; }
    make -j"$(nproc)" bindeb-pkg

    Ce garde-fou est essentiel : olddefconfig peut réintroduire des choix qui élaguent netfilter. Un noyau sans nftables ne pourra pas monter de pare-feu.

  5. Installer et régénérer GRUB

    Fenêtre de terminal
    sudo dpkg -i ../linux-image-*.deb
    sudo update-grub
    # relier les symlinks de plus haut niveau vers le noyau le plus récent
    sudo ln -sf "$(ls -1v /boot/vmlinuz-* | tail -1)" /vmlinuz
    sudo ln -sf "$(ls -1v /boot/initrd.img-* | tail -1)" /initrd.img

    Purger les noyaux cloud d'origine peut laisser les liens /vmlinuz et /initrd.img pendants (lynis KRNL-5788) : on les repointe vers le noyau le plus récent.

C'est le cas piégeux. RHEL et AlmaLinux construisent le noyau depuis le SRPM via rpmbuild, et surtout : le .config que vous éditeriez dans SOURCES/ est régénéré au %prep par process_configs.sh depuis redhat/configs/. Résultat classique : build OK, install OK, zéro option KSPP (le noyau stock, juste renommé). La parade consiste à injecter scripts/config après la ligne process_configs.sh du fichier spec, sur le config réellement utilisé par le build.

  1. Préparer l'environnement (et lever les blocages)

    Fenêtre de terminal
    export HOME=/root
    # relâcher fapolicyd et SELinux le temps du build, puis les restaurer
    systemctl stop fapolicyd 2>/dev/null || true
    setenforce 0 2>/dev/null || true
    # le build charge des modules (crypto_user pour l'étape FIPS) :
    # si kernel.modules_disabled=1, rebootez d'abord ce garde-fou
    modprobe crypto_user 2>/dev/null || true

    Un hôte déjà durci casse le build : fapolicyd refuse l'ouverture des artefacts .so non listés (vdso2c: Operation not permitted) et un système de fichiers de build fraîchement monté est non étiqueté SELinux. On relâche les deux pour le build uniquement, on les restaure ensuite. Et si un durcissement antérieur a posé kernel.modules_disabled=1, le chargement de modules est gelé et le build échoue (libkcapi ... cannot open netlink socket) : ce gel ne se lève qu'au reboot.

  2. Installer les outils et récupérer le SRPM

    Fenêtre de terminal
    sudo dnf install -y rpm-build rpmdevtools dnf-plugins-core gcc-plugin-devel
    sudo dnf config-manager --set-enabled crb 2>/dev/null \
    || sudo dnf config-manager --set-enabled powertools 2>/dev/null || true
    rpmdev-setuptree
    dnf download --source kernel
    rpm -Uvh kernel-*.src.rpm
    sudo dnf builddep -y ~/rpmbuild/SPECS/kernel.spec

    RHEL et AlmaLinux livrent le noyau sans support des plugins GCC : d'où l'installation explicite de gcc-plugin-devel. Le dépôt CRB (ex-PowerTools) est requis pour les dépendances de build.

  3. Patcher le fichier spec

    Fenêtre de terminal
    cd ~/rpmbuild/SPECS
    # NVR distinct pour ne pas entrer en collision avec le noyau stock
    sed -ri 's/^#[[:space:]]*%?[[:space:]]*define buildid .*/%define buildid .hardened/' kernel.spec
    # rendre non fatale l'étape bpftool (BTF absent sans debuginfo)
    sed -ri 's#(bpftool btf dump file vmlinux format c > .*/vmlinux.h)#\1 || :#' kernel.spec
    # include explicite dans sev.c (une config KSPP peut casser l'include transitif de slab.h)
    sed -ri '/^%build[[:space:]]*$/i find "$RPM_BUILD_DIR" -path "*/arch/x86/kernel/sev.c" -exec sed -i "/^#define pr_fmt/a #include <linux/slab.h>" {} +' kernel.spec

    Chaque patch corrige un piège réel : la collision de NVR, l'étape bpftool btf qui échoue sans debuginfo, et le fichier sev.c qui casse en -Werror quand une option KSPP supprime l'include transitif de linux/slab.h.

  4. Injecter les options KSPP après process_configs.sh

    Fenêtre de terminal
    ARCH=$(uname -m)
    # exclure les membres de Kconfig CHOICE que process_configs rejette,
    # et ajouter DEBUG_KERNEL + GCC_PLUGINS comme prérequis
    KSPP_EN=""; for o in BUG_ON_DATA_CORRUPTION DEBUG_LIST DEBUG_SG DEBUG_WX FORTIFY_SOURCE \
    HARDENED_USERCOPY PAGE_POISONING RANDOMIZE_BASE SCHED_STACK_END_CHECK \
    SECURITY_DMESG_RESTRICT SECURITY_YAMA SLAB_FREELIST_HARDENED SLAB_FREELIST_RANDOM \
    STACKPROTECTOR_STRONG STRICT_KERNEL_RWX MODULE_SIG_FORCE GCC_PLUGINS \
    GCC_PLUGIN_LATENT_ENTROPY RANDSTRUCT_FULL GCC_PLUGIN_STACKLEAK \
    INIT_ON_ALLOC_DEFAULT_ON INIT_ON_FREE_DEFAULT_ON DEBUG_KERNEL; do KSPP_EN="$KSPP_EN CONFIG_$o"; done
    KSPP_DIS=""; for o in HIBERNATION KEXEC IA32_EMULATION PROC_KCORE MODIFY_LDT_SYSCALL \
    SLAB_MERGE_DEFAULT; do KSPP_DIS="$KSPP_DIS CONFIG_$o"; done
    INJECT="_sc=\$(find \"\$RPM_BUILD_DIR\" -path '*/scripts/config' | head -1); for _c in \$(find \"\$RPM_BUILD_DIR\" -name 'kernel-*-$ARCH.config' ! -name '*-debug.config'); do for _o in$KSPP_EN; do \"\$_sc\" --file \"\$_c\" --enable \$_o; done; for _o in$KSPP_DIS; do \"\$_sc\" --file \"\$_c\" --disable \$_o; done; done"
    awk -v ins="$INJECT" '/\.\/process_configs\.sh -w -c/{print; print ins; next} {print}' kernel.spec > kernel.spec.new && mv kernel.spec.new kernel.spec

    Points clés : on exclut les membres de CHOICE (vsyscall, MODULE_SIG_SHA512) que process_configs rejette, on ajoute DEBUG_KERNEL et GCC_PLUGINS en prérequis (sinon olddefconfig drope les DEBUG_* et les plugins), et on ne force pas netfilter en builtin : les modules netfilter stock sont signés (MODULE_SIG_ALL), donc ils se chargent sous MODULE_SIG_FORCE, alors que forcer =y bascule LIBCRC32C que process_configs -w refuse.

  5. Construire, puis installer

    Fenêtre de terminal
    # kABI désactivé : RANDSTRUCT change la disposition des structures et casse l'ABI stable
    # -j6 borne la RAM : l'instrumentation des plugins double la mémoire par job (risque d'OOM)
    rpmbuild --define "_smp_mflags -j6" -bb \
    --without debug --without debuginfo --without kabidupchk --without kabichk \
    --with baseonly --target="$ARCH" kernel.spec
    sudo rpm -Uvh --force ~/rpmbuild/RPMS/"$ARCH"/kernel-core-*hardened*.rpm \
    ~/rpmbuild/RPMS/"$ARCH"/kernel-modules-*hardened*.rpm \
    ~/rpmbuild/RPMS/"$ARCH"/kernel-[0-9]*hardened*.rpm

    On installe avec rpm -Uvh --force : un noyau reconstruit avec le même NVR ne serait pas réinstallé par dnf install (no-op sur nom-version-release identique). Et --without kabichk est obligatoire : RANDSTRUCT casse volontairement la kABI, donc les modules hors-arbre compilés contre les symboles stock ne se chargeront pas, compromis assumé d'un noyau durci.

Certaines protections ne se règlent ni à la compilation ni par sysctl, mais sur la ligne de commande du noyau. Leur configuration effective se lit dans /proc/cmdline, et une modification n'entre en vigueur qu'au reboot. On les rend persistantes via un fichier dans /etc/default/grub.d/ :

Fenêtre de terminal
sudo tee /etc/default/grub.d/99-hardening.cfg >/dev/null <<'EOF'
GRUB_CMDLINE_LINUX="$GRUB_CMDLINE_LINUX l1tf=full,force mds=full pti=on spectre_v2=on \
spec_store_bypass_disable=on slab_nomerge slub_debug=P page_poison=1 \
page_alloc.shuffle=1 init_on_alloc=1 init_on_free=1 vsyscall=none"
EOF
sudo update-grub # ou grub2-mkconfig -o /boot/grub2/grub.cfg sur RHEL

Le durcissement noyau runtime (les clés sysctl comme kernel.kptr_restrict, kernel.dmesg_restrict, kernel.yama.ptrace_scope) est complémentaire et détaillé dans le guide sysctl. Le blocage des modules inutiles est traité dans désactiver des modules noyau, et la protection de GRUB dans protéger GRUB par mot de passe.

Un durcissement qui n'est pas mesuré n'existe pas, et un durcissement runtime qui ne survit pas au reboot est un faux positif. Trois vérifications complémentaires :

  • /boot/config-$(uname -r) : la configuration effective du noyau installé. C'est là qu'on relit si CONFIG_RANDSTRUCT_FULL=y, CONFIG_MODULE_SIG_FORCE=y, etc. ont réellement pris.
  • /proc/cmdline : les mitigations de boot réellement actives.
  • kernel-hardening-checker (anciennement kconfig-hardened-check, de a13xp0p0v) : il croise votre /boot/config avec les recommandations KSPP et grsecurity, par architecture et par version.
Fenêtre de terminal
pip install --user kernel-hardening-checker
kernel-hardening-checker -c "/boot/config-$(uname -r)"

Pour situer ce noyau durci dans une démarche de conformité globale, croisez-le avec un audit système complet : lynis pour le hardening index, OpenSCAP pour un profil CIS ou ANSSI, et les référentiels CIS Benchmarks et ANSSI-BP-028.

Tous ces symptômes ont une cause précise et une correction directe.

SymptômeCauseSolution
Build OK, 0 option KSPP (RHEL)config régénéré par process_configsinjecter scripts/config après cette ligne du spec
DEBUG_* et plugins absents (RHEL)dépendance manquante à olddefconfigajouter DEBUG_KERNEL et GCC_PLUGINS
changes choice state (RHEL)membre de Kconfig CHOICEexclure vsyscall et MODULE_SIG_SHA512
sev.c ... implicit declarationinclude transitif de slab.h casséinjecter #include <linux/slab.h> après pr_fmt
Nouveau noyau non installé (RHEL)collision de NVR, dnf install no-oprpm -Uvh --force
~/rpmbuild devient /rpmbuildHOME non définiexport HOME=/root
Noyau sans pare-feu (Debian)olddefconfig élague netfilterforcer builtin + garde-fou grep NF_TABLES
Build KO en ~20 s (trixie)debhelper (compat 13) manquantapt-get install debhelper
VM sans réseau après rebootvirtio_net en module ou symvers périmévirtio en builtin, arbre de build propre
VM Intel qui ne boote plusnosmt plante le démarrage SMPmds=full sans nosmt
sign-file échoue (Debian)MODULE_SIG_KEY vidégarder certs/signing_key.pem
OOM (exit 137) au build (RHEL)plugins GCC doublent la RAM par jobborner avec _smp_mflags -j6

Un noyau durci maison a un coût qu'il faut assumer en connaissance de cause. Il sort du support de votre distributeur : plus de correctifs automatiques sur ce noyau précis, c'est à vous de le reconstruire à chaque CVE. Il casse la kABI (RANDSTRUCT), donc les modules hors-arbre (pilotes propriétaires GPU, agents, DKMS compilés contre les symboles stock) ne se chargeront pas. Et MODULE_SIG_FORCE interdit tout module non signé par votre clé de build.

Sur un parc où ces contraintes posent problème, restez sur le noyau du distributeur et concentrez le durcissement sur le runtime (sysctl, modules, montages, GRUB, MAC) : vous obtenez déjà l'essentiel du bénéfice sans recompilation. Le noyau KSPP se justifie surtout sur des hôtes sensibles maîtrisés de bout en bout, où l'on accepte de gérer le cycle de vie du noyau soi-même.

  • Certaines protections KSPP sont compile-time : la seule remédiation est un noyau recompilé.
  • On recompile le noyau de la distribution, avec un sous-ensemble boot-safe (pas de MODULES=n, pas de CFI).
  • La même liste d'options sert de la 4.18 aux 6.x : les symboles absents sont ignorés, les renommés portent les deux orthographes.
  • Sous Debian/Ubuntu, bindeb-pkg suffit ; forcez netfilter en builtin et gardez un garde-fou pare-feu.
  • Sous RHEL/AlmaLinux, injectez les options après process_configs.sh, sinon le config est régénéré et le durcissement disparaît.
  • Prouvez avec /boot/config, /proc/cmdline et kernel-hardening-checker, et distinguez runtime de persistance.
  • Un noyau durci sort du support et casse la kABI : à réserver aux hôtes sensibles maîtrisés.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn