Bonnes pratiques pour l’isolation des conteneurs
Maryann Agofure
29 août 2022
0 minutes de lectureLes conteneurs sont un format standardisé de packaging logiciel qui permet d’exécuter des applications de manière prévisible et reproductible. L’isolation des conteneurs est l’un des principaux avantages des applications conteneurisées. En utilisant des conteneurs, nous pouvons isoler nos logiciels de leur environnement et ainsi améliorer la cohérence et la fiabilité de nos environnements de développement et de préproduction.
Vous connaissez probablement les conteneurs Docker, ou vous les utilisez déjà. Docker assure l’isolation en s’appuyant sur des fonctionnalités Linux comme les groupes de contrôle (généralement abrégés en cgroups), les filtres du mode de calcul sécurisé (seccomp) et les espaces de noms du noyau. Il existe aussi d’autres types de conteneurs, notamment les conteneurs sandboxés, comme gVisor, et les conteneurs virtualisés, comme AWS Firecracker.
Même si les conteneurs sont conçus pour être isolés, nous devons appliquer les bonnes pratiques nécessaires pour garantir leur isolation efficace et assurer leur sécurité. Cet article examine les implications de l’isolation des conteneurs et présente les bonnes pratiques de sécurité et les méthodes à adopter pour les conteneurs Linux, sandboxés et virtualisés.
Qu’est-ce que l’isolation des conteneurs et comment fonctionne-t-elle ?
Comme son nom l’indique, l’isolation des conteneurs consiste à isoler l’environnement d’exécution d’une application conteneurisée du système d’exploitation hôte et des autres processus qui s’y exécutent.
Cette isolation prend plusieurs formes : isolation du système de fichiers, du réseau et des appels système, ainsi qu’isolation de l’utilisation des ressources, comme le processeur et la mémoire.
Les détails techniques du fonctionnement de l’isolation dépendent du type de conteneur utilisé. Comme nous allons le voir, plusieurs options s’offrent à nous.
Différentes approches de l’isolation des conteneurs
Comme nous l’avons indiqué, cet article examine l’isolation des conteneurs et ses liens avec trois types de conteneurs : les conteneurs Linux comme Docker, les conteneurs sandboxés comme gVisor et la virtualisation légère basée sur KVM, comme AWS Firecracker.
Chaque type de conteneur aborde l’isolation différemment et isole différentes parties du système. Chacun implique des bonnes pratiques spécifiques à suivre pour isoler nos conteneurs.
Examinons les bonnes pratiques d’isolation pour chaque type de conteneur.
Conteneurs Linux
Les conteneurs Linux comme Docker utilisent des cgroups, des filtres seccomp et les espaces de noms du noyau pour isoler les conteneurs. Les cgroups permettent de limiter l’utilisation des ressources par un groupe de processus. Ils peuvent, par exemple, plafonner l’utilisation de différentes ressources, comme les E/S disque, la mémoire, le réseau, le temps processeur et même certains processeurs dans un système multicœur. Seccomp permet de filtrer tous les appels système effectués par un processus. Ce filtrage fonctionne avec la technique des espaces de noms du noyau, ce qui permet aux fonctions d’un conteneur d’avoir une vue isolée du système.
Cette approche est la plus simple, mais cela signifie aussi que Docker doit veiller à ce que toutes les applications conteneurisées configurent correctement leurs indicateurs d’espace de noms lors de leur création.
L’approche de Docker est relativement simple, donc facile à utiliser. Toutefois, cette simplicité signifie que son isolation est moins robuste que celle d’autres solutions comme gVisor. Docker doit autoriser certains appels système à franchir la frontière entre les espaces de noms. Une application peut donc toujours accéder à des informations sur l’hôte si le filtre seccomp fournit un argument valide pour un appel donné.
Heureusement, quelques bonnes pratiques permettent de tirer le meilleur parti de l’isolation des conteneurs avec Docker :
Utilisez un utilisateur dédié pour chaque conteneur en spécifiant un compte utilisateur particulier dans votre Dockerfile. Ainsi, un conteneur ne pourra pas accéder aux ressources d’un autre conteneur (par exemple, à ses fichiers et répertoires), ni les modifier. Lorsque vous créez des images Docker et des configurations runc, utilisez des utilisateurs disposant du moins de privilèges possible.
Limitez les capacités des conteneurs en utilisant les capacités Linux mentionnées précédemment. Appliquez le principe du moindre privilège : n’accordez à un conteneur que ce dont il a besoin pour accomplir ses tâches. Ce principe limite ce qu’une application exécutée dans un conteneur peut voir ou faire en dehors de son environnement isolé.
Empêchez la configuration de périphériques réseau inutiles pour chaque conteneur. Cela limite ce qu’un conteneur peut voir et faire sur l’infrastructure réseau de l’hôte.
Utilisez des restrictions cgroup pour limiter les ressources accessibles à chaque conteneur, comme les parts de processeur, les pages mémoire, la bande passante des E/S bloc, et bien plus encore.
Configurez les paramètres du noyau, comme le nombre de PID, la taille maximale de la pile et le nombre maximal de threads qu’un conteneur peut créer. Vous éviterez ainsi qu’un conteneur prenne le contrôle de l’espace de noms des PID d’un autre conteneur ou fasse planter le noyau de l’hôte en provoquant une panique.
Combinées, ces pratiques réduisent la surface d’attaque de nos conteneurs en limitant le nombre d’éléments qu’un attaquant peut exploiter. Bien sûr, la meilleure défense consiste à ne jamais exécuter de code non fiable dans nos conteneurs. Mais cela suppose de savoir exactement ce qui s’exécute dans chacun d’eux, ce qui est rarement réaliste pour des équipes de développement et DevOps très occupées, soumises à des délais serrés.
Si nous savons que nous devons exécuter un conteneur non fiable, la meilleure pratique suivante consiste à envisager un environnement d’exécution de conteneurs doté d’un modèle de sécurité plus robuste.
Conteneurs sandboxés
Les conteneurs sandboxés offrent les mêmes mécanismes d’isolation que les conteneurs Linux classiques, auxquels ils ajoutent des couches de protection supplémentaires.
gVisor, un excellent conteneur sandboxé, met en œuvre un mini-noyau personnalisé en espace utilisateur, qui s’interpose entre les applications conteneurisées et le noyau de l’hôte. Il intercepte tous les appels système du conteneur et vérifie chacun d’eux par rapport à une politique avant de le transmettre au noyau de l’hôte. gVisor met également en œuvre une pile TCP/IP personnalisée pour mieux contrôler la façon dont les charges de travail conteneurisées interagissent avec le réseau. Enfin, gVisor utilise un proxy de système de fichiers interposé entre le conteneur et le système de fichiers de l’hôte. Grâce à ces contrôles en couches, gVisor peut réduire la surface d’attaque d’un conteneur en imposant des frontières strictes, tout en restant compatible avec la plupart des applications. De plus, gVisor étant écrit en Go, un langage sûr pour la mémoire, il est bien moins susceptible de subir des dépassements de tampon et d’autres attaques que les applications écrites en C, comme le noyau Linux.
L’approche de gVisor présente toutefois quelques inconvénients. Le débogage des applications exécutées dans gVisor peut être difficile, car la plupart des outils conçus pour le noyau Linux ne sont pas utilisables. Autre inconvénient : gVisor n’utilisant pas un noyau Linux standard, certaines fonctionnalités devront peut-être être réimplémentées pour prendre en charge certaines charges de travail.
gVisor et les autres conteneurs sandboxés offrent une sécurité supérieure à celle des conteneurs Linux classiques. Cependant, adopter un modèle de conteneur offrant une isolation encore plus robuste peut s’avérer la meilleure pratique à suivre.
Machines virtuelles légères
Contrairement aux conteneurs Linux et aux conteneurs sandboxés, les conteneurs basés sur des machines virtuelles (VM) légères, comme AWS Firecracker, adoptent une approche totalement différente. Ils utilisent un hyperviseur, comme KVM ou qemu, pour créer des VM légères (généralement appelées microVM) pour chaque conteneur. Cette isolation robuste réduit au minimum la surface d’attaque au sein des microVM invitées, ce qui la rend facile à contrôler.
Cette technique rend très difficile l’accès d’un attaquant aux informations privilégiées de l’hôte, mais limite aussi les interactions possibles entre un conteneur et l’hôte. Par exemple, les conteneurs exécutés dans des microVM ne peuvent pas partager directement de fichiers avec l’hôte.
Les microVM ont une surface d’attaque plus réduite que les conteneurs exécutés sur des VM classiques, car elles ne prennent en charge qu’un sous-ensemble des périphériques matériels gérés par les noyaux Linux ordinaires.
En définitive, les microVM sont le meilleur choix lorsque nous avons besoin d’une très forte isolation entre les conteneurs, par exemple pour exécuter du code non fiable provenant de plusieurs locataires sur le même serveur.
Bien choisir son approche de l’isolation des conteneurs
Quel que soit le type de conteneur utilisé, il n’existe pas de solution universelle pour l’isolation des conteneurs. Nous devons donc trouver un équilibre entre les facteurs suivants, en fonction des besoins propres à chaque projet.
Performances. Les conteneurs Linux offrent les meilleures performances, car ils comportent le moins de couches d’indirection entre le conteneur et le système d’exploitation hôte. Les conteneurs sandboxés sont sensiblement plus lents, en raison des couches intermédiaires supplémentaires entre le conteneur et le système d’exploitation hôte. Les microVM ont l’impact le plus important sur les performances, car elles doivent fournir une interface matérielle virtualisée à chaque conteneur.
Sécurité. Plus les capacités d’isolation des conteneurs augmentent, plus nos conteneurs sont sécurisés. Comme nous l’avons vu, les conteneurs sandboxés sont plus sûrs que les conteneurs Linux, et les conteneurs basés sur des microVM sont plus sûrs que les deux autres.
Complexité et durée de développement. Les conteneurs Linux sont relativement simples et constituent la majorité des conteneurs exécutés en production. Les outils et la documentation de qualité ne manquent pas : ils réduisent la durée de développement et la complexité du déploiement d’applications conteneurisées. Les conteneurs sandboxés et les microVM sont beaucoup moins répandus, et de nombreux outils courants ne les prennent donc pas en charge. Leur popularité moindre se traduit par une prise en charge plus limitée des outils et des plateformes, ce qui augmente la durée et la complexité du développement : les équipes de développement et DevOps doivent effectuer davantage de tâches manuellement au lieu de s’appuyer sur des outils.
En définitive, il faut trouver un compromis entre performances, sécurité et complexité. Privilégier les performances peut, par exemple, nous obliger à renoncer à une partie de la sécurité. À l’inverse, privilégier la sécurité peut impliquer de sacrifier des performances et d’allonger la durée de développement.
Sécurité des conteneurs
En tant que développeurs et spécialistes DevOps, nous devons savoir comment nos conteneurs sont isolés de nos machines hôtes afin de comprendre les limites de cette isolation. Les conteneurs accélèrent considérablement les cycles de développement, mais il est important d’appliquer les bonnes pratiques pour protéger nos conteneurs et nos applications.
Toutes les équipes de développement et DevOps devraient adopter une stratégie de défense en profondeur : l’isolation des conteneurs n’en est qu’un élément. Vous pouvez améliorer la sécurité de vos systèmes en veillant à ce que votre code ne présente aucune vulnérabilité, à l’aide d’un scanner de code et d’un scanner de conteneurs pour protéger leur contenu. Après tout, l’isolation des conteneurs doit constituer une ligne de défense, et non votre unique ligne de défense.
La sécurité des conteneurs, pensée pour les développeurs
Snyk détecte et corrige automatiquement les vulnérabilités dans les images de conteneurs et les workloads Kubernetes.
