Aller au contenu
English
Virtualisation medium

Sécuriser KVM : sVirt, Secure Boot, vTPM et filtrage réseau

45 min de lecture

Logo KVM

Une VM compromise ne doit pas donner accès à l'hôte, ni à ses voisines. KVM s'appuie pour cela sur plusieurs couches qui travaillent déjà chez vous, souvent sans que vous le sachiez : un utilisateur dédié par machine, un label sVirt dérivé de son identifiant, un firmware qui peut refuser un noyau non signé. Ce guide montre comment vérifier que ces couches sont réellement actives, car une protection chargée n'est pas forcément une protection qui bloque.

  • Lire les couches de confinement que libvirt applique à chaque machine
  • Vérifier que le profil sVirt est en mode bloquant, et non simplement chargé
  • Activer le Secure Boot et savoir ce que la NVRAM contient
  • Ajouter un vTPM, et ce que cela implique pour vos sauvegardes
  • Filtrer le trafic d'une VM avec nwfilter, au plus près de la machine
  • KVM/libvirt installé (guide installation)
  • Une VM définie, même arrêtée, pour lire son XML
  • Les droits d'administration sur l'hôte

Regardez le XML d'une machine qui tourne : libvirt y écrit ce qu'il a réellement mis en place.

Fenêtre de terminal
virsh dumpxml ma-vm | grep -A3 '<seclabel'

Sur l'hôte de ce guide, une Ubuntu 24.04, la réponse contient deux blocs :

<seclabel type='dynamic' model='apparmor' relabel='yes'>
<label>libvirt-7fe39141-9708-4375-b960-8bba34cd58f2</label>
</seclabel>
<seclabel type='dynamic' model='dac' relabel='yes'>
<label>+64055:+994</label>
</seclabel>
CoucheCe qu'elle fait
dacLe processus QEMU tourne sous un utilisateur et un groupe dédiés, ici 64055:994, pas sous root
apparmorUn profil par machine, nommé d'après l'UUID du domaine

C'est sVirt : le label n'est pas commun à toutes les VM, il est propre à chacune. Deux machines du même hôte portent deux labels différents, et le profil de l'une ne l'autorise pas à toucher les fichiers de l'autre. Le type='dynamic' indique que libvirt crée et retire ce label au démarrage et à l'arrêt, sans que vous ayez rien à écrire.

Le contrôle qui compte : confiner, ou seulement observer

Section intitulée « Le contrôle qui compte : confiner, ou seulement observer »

Un profil chargé n'est pas un profil qui bloque. AppArmor distingue deux modes, et la différence décide de tout : en enforce, une violation est refusée ; en complain, elle est simplement journalisée et l'action aboutit quand même.

Fenêtre de terminal
sudo aa-status | grep -c 'libvirt-'
ps -eo label,comm | grep qemu-system

La seconde commande est la plus parlante, car elle donne le mode réellement appliqué au processus en cours :

libvirt-7fe39141-9708-4375-b960-8bba34cd58f2 (complain) qemu-system-x86

Ce (complain) change tout. Sur l'hôte qui a servi à écrire ce guide, 18 profils libvirt sont en mode plainte contre 9 en mode bloquant : la majorité des machines n'y sont donc pas confinées, elles sont observées. Le mécanisme est en place, les journaux se remplissent, et rien n'empêche une violation d'aboutir.

sVirt est la couche visible, celle qui apparaît dans le XML. Trois autres travaillent sans rien y écrire, et elles se vérifient sur le processus lui-même. Relevez d'abord son identifiant :

Fenêtre de terminal
PID=$(pgrep -f 'qemu-system-x86.*ma-vm')

libvirt lance chaque QEMU dans son propre espace de montage. Le résultat est spectaculaire, et se mesure en une commande :

Fenêtre de terminal
ls /dev | wc -l # sur l'hôte
sudo nsenter -t "$PID" -m ls /dev | wc -l # vu par QEMU

Sur l'hôte de ce guide, /dev compte 235 entrées. Le QEMU d'une VM n'en voit que onze :

full hugepages kvm mqueue null ptmx pts random shm urandom zero

Ni les disques de l'hôte, ni les périphériques USB, ni rien d'autre que le strict nécessaire. Un QEMU compromis ne peut pas ouvrir ce qu'il ne voit pas. Le réglage qui gouverne cela est namespaces = [ "mount" ] dans qemu.conf.

QEMU tourne sous un filtre seccomp, qui interdit les appels système dont il n'a pas besoin et réduit d'autant la surface offerte au noyau :

Fenêtre de terminal
grep Seccomp /proc/$PID/status

La valeur Seccomp: 2 signifie qu'un filtre est actif. libvirt l'applique avec une politique qui refuse les appels obsolètes, l'élévation de privilèges, la création de processus et le contrôle des ressources. Le réglage correspondant est seccomp_sandbox = 1.

Fenêtre de terminal
grep CapEff /proc/$PID/status

La réponse est 0000000000000000 : le processus ne conserve aucun privilège particulier. Même s'il parvenait à sortir de son périmètre, il ne pourrait ni charger un module, ni ouvrir un port privilégié, ni changer d'identité.

Ce cloisonnement répond au modèle de menace que QEMU assume explicitement : l'invité est considéré comme non fiable, au même titre que les images de disque, les protocoles réseau et les périphériques passés en direct. L'objectif déclaré du projet est qu'un invité malveillant ne puisse ni prendre le contrôle d'un autre invité, ni accéder à ses données.

Le confinement protège l'hôte d'une VM compromise. Il ne protège pas d'un administrateur de l'hyperviseur : qui contrôle l'hôte lit les disques, attache un périphérique, ouvre une console. C'est pourquoi l'appartenance au groupe libvirt est un pouvoir d'administration, et se décide comme tel. Les autorisations fines par action passent par polkit, traité dans le guide d'installation.

Le Secure Boot fait vérifier par le firmware la signature de ce qu'il démarre. Pour une VM, cela suppose un firmware UEFI capable de le faire, et un jeu de variables contenant les clés reconnues.

Fenêtre de terminal
ls /usr/share/OVMF/

Les quatre fichiers qui comptent sur une Ubuntu récente :

FichierRôle
OVMF_CODE_4M.fdFirmware sans Secure Boot
OVMF_CODE_4M.secboot.fdFirmware avec Secure Boot
OVMF_VARS_4M.ms.fdVariables préchargées des clés Microsoft, celles qui signent les distributions courantes
OVMF_VARS_4M.snakeoil.fdClés de test, jamais en production

À la création, virt-install accepte de déclarer le tout :

Fenêtre de terminal
virt-install ... --boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=yes

La NVRAM mérite une attention particulière : c'est un fichier par machine, créé à partir du gabarit de variables, et il contient les clés et les entrées d'amorçage propres à cette VM. Il vit à côté du disque, en général sous /var/lib/libvirt/qemu/nvram/.

Un TPM virtuel offre à l'invité ce qu'une puce TPM offre à une machine physique : un stockage scellé pour des clés, utilisé par BitLocker, par systemd-cryptenroll ou par une attestation. Il est fourni par swtpm, un émulateur indépendant de QEMU.

Fenêtre de terminal
swtpm --version

La déclaration tient en trois lignes de XML :

<tpm model='tpm-crb'>
<backend type='emulator' version='2.0'/>
</tpm>

libvirt crée alors un état persistant pour ce TPM, sous /var/lib/libvirt/swtpm/, propre à la machine et indispensable à son fonctionnement ultérieur.

Un pare-feu sur l'hôte filtre ce qui traverse le pont. nwfilter agit un cran plus bas, sur l'interface virtuelle elle-même, ce qui protège aussi les machines les unes des autres sur le même réseau.

Fenêtre de terminal
virsh nwfilter-list

libvirt livre des filtres tout faits, dont le plus utile s'attache en une ligne au XML de l'interface :

<interface type='network'>
<source network='default'/>
<filterref filter='clean-traffic'/>
</interface>

clean-traffic interdit à la VM d'usurper son adresse MAC ou son adresse IP, et bloque les annonces ARP mensongères. Sur un réseau où plusieurs machines de confiance inégale cohabitent, c'est la protection qui empêche l'une d'écouter le trafic des autres.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • libvirt applique deux couches par défaut : un utilisateur dédié (dac) et un label sVirt par machine, dérivé de son UUID.
  • Ce label est propre à chaque VM : le profil de l'une ne lui permet pas de toucher les fichiers de l'autre.
  • Un profil chargé n'est pas un profil qui bloque. Le mode (complain) lu sur le processus signifie observation, pas confinement. Sur l'hôte de ce guide, 18 profils sur 27 étaient dans ce cas.
  • Le confinement ne protège pas d'un administrateur de l'hyperviseur : le groupe libvirt est un pouvoir d'administration, polkit permet de le découper.
  • Le Secure Boot demande un firmware secboot et un jeu de variables : OVMF_VARS_4M.ms.fd pour les clés Microsoft, jamais snakeoil en production.
  • Une VM UEFI ou vTPM possède un état hors du disque, NVRAM et swtpm : une sauvegarde qui les oublie ne restaure pas une machine qui démarre.
  • clean-traffic empêche une VM d'usurper une adresse MAC ou IP, au niveau de son interface et non du pont.

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