Blog

Chaque locataire devrait-il avoir sa propre VM ?

Choisissez entre des conteneurs par locataire, des VM et des configurations hybrides grâce à un cadre de décision basé sur les risques et aux étapes de durcissement qui rendent chaque option défendable.

Résumé

L'hébergement multi-locataires vous oblige à choisir jusqu'où les locataires peuvent accéder les uns aux autres. Les conteneurs utilisent les namespaces Linux et les cgroups pour isoler les processus et les ressources, mais ils partagent le noyau hôte. Les machines virtuelles ajoutent une frontière au niveau matériel, au prix de la rapidité et de la charge opérationnelle. Une approche hybride — des conteneurs dans des VM — peut vous offrir les deux, mais elle double la surface que vous devez corriger. Cet article vous guide à travers une décision basée sur les risques, une comparaison côte à côte, et les étapes de durcissement Docker qui comptent même à l'intérieur d'une VM. À la fin, vous saurez quel modèle d'isolation convient à vos locataires et quoi configurer avant le lancement.

Votre application multi-locataires est presque prête. Vous avez un fichier Docker Compose qui démarre une pile par client, et c'est rapide. Puis un ami qui dirige une entreprise d'hébergement demande : « Est-ce que vous donnez à chaque locataire sa propre VM ? » Vous figez. Vous n'aviez pas prévu cette question. Cet article vous donne un moyen d'y répondre aujourd'hui, sans équipe de sécurité. Vous faites cela seul, donc la décision doit être assez simple à défendre à 2 h du matin.

Arrêtez d'essayer de trouver le « meilleur » modèle. Commencez par écrire ce qui se passe si le code d'un locataire prend le contrôle de votre hôte. Définissez le rayon d'impact avant de choisir un outil. Cet exercice vous en apprendra plus que n'importe quel benchmark.

Le noyau est le colocataire que vous ne pouvez pas expulser

Les conteneurs sont efficaces parce qu'ils partagent le noyau hôte. Ce partage est tout l'astuce, et tout le risque. Les namespaces Linux donnent à chaque conteneur sa propre vision des processus, du réseau et du système de fichiers. Les cgroups vous permettent de limiter le CPU, la mémoire et les E/S disque afin qu'un locataire ne puisse pas affamer les autres. Mais aucun ne crée un mur matériel.

Considérez un conteneur comme un processus avec une très bonne fausse identité. Il croit être sur sa propre machine. Le noyau, cependant, est une copie de Linux qui tourne sur votre hôte. Si un locataire exploite une vulnérabilité du noyau, les namespaces deviennent des métadonnées et rien de plus. Un attaquant qui peut appeler des fonctions du noyau peut atteindre d'autres namespaces sur le même noyau. C'est la fuite de conteneur dont vous entendez toujours parler.

Supposons que vous hébergez un petit outil B2B avec un conteneur par client. Un client installe un plugin douteux avec un bug d'exécution de code à distance. Avec les paramètres Docker par défaut, ce processus s'exécute en tant que root dans le conteneur. Root dans un conteneur est toujours l'UID 0, et le noyau ne distingue pas cet UID du root hôte à moins que vous ne mappiez explicitement les utilisateurs. L'attaquant peut tenter de s'échapper, et le noyau partagé est sa cible.

La défaillance n'a pas besoin d'être dramatique. Un seul locataire qui fuit de la mémoire peut pousser l'hôte dans le swap, ralentissant tous les autres locataires. Sans limites de cgroups, une boucle qui se comporte mal est une attaque de disponibilité. Avec elles, c'est un processus bloqué et une alerte.

Cela signifie-t-il que les conteneurs ne sont pas sûrs ? Non. Cela signifie que vous devez traiter le noyau comme une zone de confiance partagée. Avant de choisir, rédigez une déclaration de risque en un paragraphe : « Si le conteneur d'un locataire est compromis, l'attaquant peut accéder à : [liste]. Le coût commercial serait de : [montant ou impact]. » Si ce paragraphe vous fait peur, vous n'êtes pas paranoïaque. Vous êtes honnête.

Pour un examen plus approfondi du spectre de l'isolation, des conteneurs partagés aux piles entièrement séparées, consultez notre guide sur la conception d'une architecture Docker multi-locataires.

Trois façons de découper (choisissez-en une avant de déployer)

Il existe vraiment trois architectures pour l'isolation multi-locataires. Chaque « meilleure pratique » est une combinaison de celles-ci.

ApprocheBarrière d'isolationMeilleur quandÉcueil le plus difficile
Conteneurs par locataireNamespaces du noyau + cgroupsDe nombreux petits locataires, faible risque par locataire, besoin de densitéUne seule exploitation du noyau peut casser tous les locataires de cet hôte
Une VM par locataireVirtualisation hyperviseur/matérielleDonnées réglementées, locataires hostiles, valeur élevée par locatairePlus lourd, plus lent à provisionner, vous corrigez un OS par locataire
Conteneurs dans des VMFrontière VM autour de charges de travail conteneuriséesDensité plus une coque dure entre les groupesCoûts et frais opérationnels presque doublés

Conteneurs par locataire. C'est le choix par défaut de la plupart des fondateurs de SaaS. Chaque locataire a son propre conteneur ou une petite pile Compose. Le provisionnement est instantané, les images sont petites, l'IC/CD est simple. Les limites de ressources empêchent les voisins bruyants de dévorer le serveur. Le compromis est le noyau partagé. Si vous pouvez maintenir des charges de travail non privilégiées et corriger régulièrement l'hôte, c'est souvent la bonne première décision.

Ne mettez pas deux locataires dans le même conteneur. C'est un noyau partagé plus un runtime partagé plus un système de fichiers partagé. Si un locataire télécharge un fichier qui crée un processus, l'autre locataire est déjà dans la même table des processus. Un conteneur est votre unité d'isolation ; faites un locataire par conteneur.

Et la base de données ? Si chaque locataire se connecte à une instance MongoDB ou PostgreSQL avec les mêmes identifiants, vous avez déjà ajouté un énorme composant partagé. Donnez à chaque locataire des identifiants séparés, et idéalement une base de données ou un schéma séparé. Les conteneurs isolent l'application ; la base de données est souvent la première fuite qu'un attaquant testera.

Une VM par locataire. Donnez à chaque locataire une machine virtuelle complète. L'hyperviseur ajoute une frontière au niveau matériel, ce qui est exactement ce qu'une exploitation du noyau doit franchir pour atteindre l'hôte. C'est important pour les environnements réglementés ou lorsque les locataires ne sont pas dignes de confiance. Le coût est la densité et le temps. Vous gérez désormais une flotte de systèmes d'exploitation, pas seulement des conteneurs. Chaque VM a besoin de mises à jour, d'agents de sécurité et de surveillance. Pour un fondateur solo, c'est un vrai travail.

Les schémas qui fonctionnent à ce niveau : utilisez l'infrastructure en tant que code pour créer une VM à partir de la même image de base, intégrez les mises à jour dans de nouvelles images plutôt que de corriger des systèmes en direct, et arrêtez les charges de travail que vous ne reconnaissez pas. Gardez le port de gestion de la VM fermé à Internet.

Conteneurs dans des VM. Cette approche hybride est rarement abordée dans les tutoriels pour débutants. Vous placez une petite VM autour de chaque locataire (ou petit groupe de locataires), puis vous exécutez des conteneurs à l'intérieur de cette VM. La VM est un conteneur de rayon d'impact ; les conteneurs ne sont que des unités déployables. Cela vous donne la frontière dure de la virtualisation et la reproductibilité des images. Cela coûte plus cher, car vous payez pour les frais généraux de virtualisation et la flexibilité des conteneurs, mais cela peut être le modèle le plus sain à long terme lorsque vous ne pouvez pas entièrement faire confiance aux locataires.

Un micro-exemple courant : un locataire exécute une API Node et un worker en arrière-plan. Au lieu d'un énorme conteneur avec les deux processus, utilisez une VM, puis deux conteneurs avec des limites de ressources différentes, un réseau partagé et aucune exposition directe à Internet pour le worker. La VM fournit la frontière dure ; les conteneurs fournissent la structure.

Lequel devriez-vous choisir ? Le tableau est votre liste restreinte. Les sections suivantes rendent la décision concrète.

Si vous choisissez les conteneurs, faites ces six choses ou ne vous embêtez pas

Les conteneurs par locataire sont acceptables si vous traitez chaque conteneur comme un attaquant potentiel. Cela commence par la configuration, pas par des vœux pieux.

0. Plafonnez les ressources avant de faire confiance à qui que ce soit. Les cgroups sont un mécanisme d'équité et une défense de disponibilité. Définissez --memory et --cpus par conteneur. Un locataire qui fuit de la mémoire devrait atteindre sa propre limite, pas celle de votre serveur. Ce n'est pas une frontière de sécurité, mais un voisin bruyant est une attaque sans une seule ligne de code. Un point de départ pratique : --memory 512m --cpus 0.5. Pour un processus worker, commencez plus bas et augmentez.

1. Exécutez-vous en tant qu'utilisateur non root. Ne laissez jamais le processus du conteneur utiliser l'UID 0, sauf si vous en avez absolument besoin. Définissez un utilisateur dans le Dockerfile et passez --user comme garde supplémentaire. Une exploitation s'exécutant en tant qu'utilisateur non privilégié a beaucoup moins de chemins vers le noyau. Dans votre Dockerfile, créez un utilisateur : RUN useradd -u 10001 app et USER app. Ne sautez pas cette étape pour gagner du temps.

2. Supprimez toutes les capacités dont vous n'avez pas besoin. Les capacités Linux divisent le pouvoir de root en petits morceaux. La plupart des applications web n'en ont presque aucune. Commencez avec --cap-drop=ALL et rajoutez uniquement ce dont vous savez avoir besoin. Un conteneur sans CAP_SYS_ADMIN est beaucoup plus difficile à utiliser pour des astuces de namespace. Si votre application essaie de lier un port privilégié, exécutez-la sur un port élevé et placez un proxy devant au lieu d'accorder NET_BIND_SERVICE.

3. Rendez le système de fichiers en lecture seule. Votre application ne doit pas écrire dans sa propre couche de conteneur. Montez un tmpfs pour l'état. Un attaquant qui ne peut pas écrire sur le disque a beaucoup plus de mal à installer une persistance. Une application PHP compromise qui essaie d'écrire un webshell échouera lorsque le système de fichiers racine est en lecture seule. Vous pouvez monter un volume nommé pour un répertoire inscriptible dont votre application a réellement besoin.

4. Appliquez seccomp et AppArmor ou SELinux. Ceux-ci envoient les appels système risqués à la pile de rejet. Docker est livré avec un profil seccomp par défaut ; utilisez-le. Ajoutez un profil AppArmor pour une couche supplémentaire. Vous n'avez pas besoin de maîtriser chaque appel système. Vous devez refuser ce qu'un worker web normal n'exige jamais. Ne lancez jamais avec --privileged. Ce drapeau désactive presque toutes les défenses que vous venez de configurer.

5. Segmentez le réseau. Ne donnez pas à chaque conteneur une route vers tous les autres conteneurs. Refus par défaut, puis n'ouvrez que les ports dont vous avez besoin. Un conteneur de base de données compromis ne devrait pas pouvoir analyser votre panneau d'administration. Si les locataires sont dans des réseaux séparés, une brèche dans un réseau ne peut pas se propager latéralement.

Un point de départ pratique :

docker run --user 10001 --cap-drop=ALL --security-opt no-new-privileges --read-only --tmpfs /tmp:rw,size=64M --security-opt seccomp=default.json --memory 512m --cpus 0.5 myimage

Placez les mêmes drapeaux dans un fichier Compose et appliquez-les à chaque locataire. Ce n'est pas complet, mais c'est un défaut beaucoup plus solide que ce que docker run vous donne dès le départ.

Pour une présentation plus approfondie, utilisez notre guide de durcissement étape par étape pour les conteneurs Docker dans l'hébergement multi-locataires.

L'isolation de conteneur améliorée de Docker est l'exception que vous devez connaître

Si vous exécutez dans un environnement Docker géré, recherchez l'isolation de conteneur améliorée de Docker (ECI). Elle utilise l'isolation des namespaces utilisateur et un runtime de conteneur sécurisé sous le capot. Root à l'intérieur d'un conteneur est mappé à un utilisateur non privilégié sur l'hôte, donc même un conteneur s'exécutant en tant que root n'obtient pas les privilèges root de l'hôte. Elle bloque également les capacités dangereuses et les appels système par défaut. Ce n'est pas quelque chose que vous pouvez recréer avec quelques drapeaux sur Docker standard. Si votre plateforme le prend en charge, activez-le. Cela ne supprime pas le besoin d'utilisateurs non root et de limites de ressources, mais cela change la donne en matière de risque.

Vous pouvez approximer une partie de cela avec le remappage des namespaces utilisateur (userns-remap) dans le démon Docker. Ce n'est pas aussi complet qu'un runtime sécurisé, mais c'est mieux que rien. Si vous l'utilisez, vérifiez que le mappage UID fonctionne avant de lui faire confiance.

La fausse idée sur les VM : passer aux machines virtuelles n'est pas du durcissement

Voici la partie à contre-courant, et c'est la partie que la plupart des gens sautent. Si vous passez à une VM par locataire et que vous déployez ensuite vos conteneurs normaux à l'intérieur, vous n'avez pas supprimé votre problème de sécurité des conteneurs. Vous avez ajouté une large cage. La fuite de conteneur fonctionne toujours ; l'attaquant atterrit simplement dans la VM au lieu de l'hôte. C'est une réelle amélioration, mais vous avez toujours besoin des six étapes.

L'autre piège est de supposer que la VM elle-même est sûre. Une image par défaut avec un mot de passe SSH faible, des paquets de base non corrigés ou un port de gestion ouvert est un cadeau. La frontière de l'hyperviseur n'a d'importance que si l'invité est durci et mis à jour. Sinon, votre « VM sécurisée » est un chemin plus rapide vers la compromission, car vous vous sentez en sécurité et arrêtez de vérifier.

Ce qu'une VM vous apporte, c'est un rayon d'impact réductible. Le désastre d'un locataire reste dans une VM. Ce que cela vous coûte, c'est votre temps. Vous devenez l'administrateur système d'autant de systèmes d'exploitation que vous avez de locataires. Si vous êtes un fondateur solo qui livre un produit, demandez-vous si vous avez les heures pour corriger et surveiller une flotte. Si oui, une VM par locataire peut être le bon choix. Si non, des conteneurs avec un durcissement solide sont peut-être plus honnêtes.

N'oubliez pas non plus que votre hôte hyperviseur est une cible critique. Un hyperviseur compromis peut voir tous les invités. Corrigez l'hôte, pas seulement les invités. La VM ne vous dispense pas de corriger l'hôte ; elle augmente les enjeux si vous ne le faites pas.

Une mise en garde sur l'approche hybride : ne supposez pas que les conteneurs dans une VM vous donnent « deux couches de sécurité » gratuitement. La VM ajoute une frontière ; le conteneur a toujours besoin de non-root, de capacités et de seccomp. Sinon, la première couche n'est aussi solide que le conteneur le plus faible.

Quatre questions qui tranchent le débat en dix minutes

N'optimisez pas dans l'abstrait. Posez-vous ces quatre questions dans l'ordre. Écrivez les réponses.

1. À quoi mon locataire a-t-il accès ? Si un locataire ne peut atteindre que sa propre application web et sa base de données, les conteneurs par locataire avec des règles réseau strictes sont défendables. Si les données d'un locataire sont réglementées ou financièrement sensibles, orientez-vous vers les VM.

2. Combien me coûterait la compromission d'un locataire ? Additionnez les clients perdus, les risques juridiques et la confiance. Si le nombre est plus grand que le coût d'exécution des VM, dépensez l'argent. Sinon, les conteneurs sont un choix rationnel.

3. Combien de locataires ai-je et combien paient-ils ? De nombreux petits abonnés : la densité des conteneurs compte. Une poignée de grands comptes : donnez à chacun une VM et facturez en conséquence. Les locataires qui vous paient moins qu'un café ne devraient pas chacun exiger un OS à gérer.

4. Puis-je corriger les choses selon un calendrier ? Les conteneurs partagent un seul noyau hôte, donc corriger l'hôte protège tout le monde. Les VM multiplient vos cibles de correction. Si vous savez que vous sauterez les mises à jour, choisissez l'architecture avec moins de pièces mobiles et des défauts plus stricts.

Vos réponses se regrouperont. Deux réponses ou plus axées sur les VM signifient que vous ne devriez pas vous rabattre par défaut sur les conteneurs par locataire. Trois réponses ou plus axées sur les conteneurs signifient que les VM sont prématurées. Un résultat contre-intuitif : un locataire à faible revenu ayant accès à des données sensibles a toujours besoin de la VM, car le coût réglementaire n'a rien à voir avec ce qu'il paie.

Lancez le minimum auquel vous pouvez faire confiance, puis gagnez plus d'isolation

Votre première architecture ne doit pas être votre dernière. Commencez avec la configuration la plus stricte que vous pouvez réellement maintenir, puis ajoutez de l'isolation à mesure que votre base de locataires le justifie. Pour la plupart des opérateurs solo, cela signifie des conteneurs par locataire avec non-root, des capacités plafonnées, des systèmes de fichiers en lecture seule, seccomp et une segmentation du réseau. Pour les locataires réglementés ou à forte valeur, passez directement à une VM par locataire, avec des conteneurs uniquement comme couche d'empaquetage à l'intérieur.

Quoi que vous choisissiez, écrivez la décision et revenez-y chaque trimestre. Lorsque vous recevrez votre première question « devrions-nous déplacer ce locataire vers une VM ? », vous aurez une réponse et la liste de contrôle pour la justifier. C'est ce que l'isolation signifie réellement : un compromis que vous gérez, pas une technologie que vous achetez.

Avant le lancement, parcourez notre liste de contrôle pratique de sécurité d'isolation Docker — elle transforme ces décisions en une liste que vous pouvez vérifier avant de montrer une page à un client.

Sources (5)