Blog

L'illusion de la confiance : comment notre configuration Docker multi-locataire a fui des données (et comment nous l'avons corrigée)

Découvrez comment la configuration Docker naïve d'une équipe a entraîné une fuite de données entre locataires et la stratégie d'isolation en couches qui l'a empêchée.

Résumé

Les conteneurs Docker ne sont pas isolés par défaut : ils partagent le noyau hôte, et sans configuration délibérée, les locataires peuvent interférer entre eux. Cet article décrit un scénario réel où un fournisseur d'hébergement multi-locataire a découvert que les conteneurs clients pouvaient accéder aux bases de données des autres en raison d'un réseau partagé et de paramètres de sécurité par défaut faibles. Nous montrons les modifications étape par étape qui ont corrigé la brèche : réseaux définis par l'utilisateur par locataire, utilisateurs non root, capacités supprimées, systèmes de fichiers en lecture seule et profils seccomp. Une hypothèse courante est que les conteneurs offrent intrinsèquement une isolation forte ; nous la remettons en question en expliquant pourquoi les machines virtuelles offrent encore une frontière plus dure et quand envisager une approche hybride. La conclusion renforce que l'isolation est un exercice en couches, pas une simple case à cocher.

L'incident : quand les conteneurs parlent trop

Vous avez configuré Docker sur un seul hôte pour exécuter plusieurs sites web clients. Chaque client a son propre conteneur — un environnement propre et isolé, n'est-ce pas ? C'est ce que nous pensions. Jusqu'à ce qu'un audit de sécurité de routine révèle que le conteneur du client A lisait le socket MySQL du conteneur du client B sur le même hôte. Ils partageaient le réseau bridge par défaut. Pire, les conteneurs s'exécutaient en tant que root, donc un attaquant qui en compromettait un pouvait altérer le socket Docker de l'hôte ou le système de fichiers d'un autre conteneur. La brèche n'était pas une exploitation sophistiquée ; c'était une mauvaise configuration de base. Des données ont fui. La confiance s'est évaporée.

Le scénario de défaillance n'est pas rare. De nombreuses équipes supposent que les namespaces et les cgroups de Docker cloisonnent automatiquement les locataires, mais elles sous-estiment le nombre de portes de sortie qui restent ouvertes par défaut. Les réseaux bridge par défaut n'offrent aucune isolation réseau entre les conteneurs. L'exécution en tant que root donne au conteneur plus de pouvoir que nécessaire. Et sans limites de ressources explicites, un voisin bruyant peut priver les autres de CPU ou de mémoire.

Étape 1 : Arrêter de partager un seul réseau

Notre première correction a été de donner à chaque locataire son propre réseau Docker défini par l'utilisateur. Cela empêche les conteneurs de se joindre sauf si vous les connectez explicitement. Nous avons créé un script qui, pour chaque locataire, lance un réseau dédié et y attache son conteneur d'application. Le conteneur de base de données vit dans le même réseau locataire, mais nous avons également ajouté un réseau interne pour la communication intra-locataire uniquement. Plus d'espionnage entre locataires.

Nous avons également isolé les bases de données en les exécutant dans des conteneurs séparés sur le même réseau locataire, en utilisant des volumes de données distincts. Cela garantit que même si un attaquant pénètre dans le conteneur d'application, il ne peut pas renifler le trafic de base de données d'un autre locataire.

Pour une plongée plus approfondie dans les stratégies d'isolation réseau, consultez Une checklist pratique de sécurité d'isolation Docker pour l'hébergement multi-locataire.

Étape 2 : Supprimer les privilèges inutiles

Par défaut, les conteneurs Docker s'exécutent avec un ensemble limité de capacités Linux, mais ils en ont encore plus que la plupart des applications n'en ont besoin. Nos conteneurs s'exécutaient en tant que root, ce qui permettait aux processus internes d'effectuer des actions comme monter des systèmes de fichiers ou modifier des paramètres du noyau. Nous sommes passés à l'exécution de l'application en tant qu'utilisateur non root dans le conteneur (en utilisant la directive USER dans le Dockerfile) et avons supprimé toutes les capacités sauf celles absolument nécessaires. Pour une application web typique, cela pourrait être seulement NET_BIND_SERVICE (pour lier les ports inférieurs à 1024) et CHOWN (pour écrire dans les répertoires). Nous avons également ajouté --security-opt no-new-privileges pour empêcher l'escalade de privilèges.

Cette seule étape a éliminé de nombreux vecteurs d'évasion de conteneurs courants. Un attaquant qui compromet le serveur web ne peut pas installer de paquets, modifier des binaires système ou accéder au socket Docker de l'hôte car le processus manque des capacités CAP_SYS_ADMIN ou CAP_DAC_OVERRIDE.

Étape 3 : Verrouiller le système de fichiers

Les systèmes de fichiers inscriptibles sont une surface d'attaque courante. Nous avons rendu le système de fichiers racine en lecture seule (--read-only) pour tous les conteneurs, puis monté des systèmes de fichiers temporaires (tmpfs) pour les répertoires nécessitant un accès en écriture, comme /tmp et le répertoire de cache de l'application. Cela empêche un attaquant de modifier le code de l'application ou de persister des binaires malveillants.

De plus, nous avons utilisé l'option --mount de Docker pour lier des répertoires sensibles comme le socket Docker uniquement lorsque cela est absolument nécessaire — et jamais sur les conteneurs de production. Le principe : si le conteneur n'a pas besoin d'écrire sur un chemin, rendez-le en lecture seule.

Étape 4 : Appliquer des profils Seccomp et AppArmor

Les profils seccomp par défaut bloquent déjà de nombreux appels système dangereux, mais nous les avons personnalisés davantage pour mettre sur liste blanche uniquement les appels système dont notre application a réellement besoin. C'est un compromis car cela nécessite de profiler l'application. Une approche plus simple consiste à utiliser le profil seccomp par défaut de Docker, puis à ajouter --security-opt seccomp=path/to/profile.json si vous avez besoin de règles plus strictes. De même, les profils AppArmor peuvent confiner les processus du conteneur à des chemins de fichiers et des capacités spécifiques. Nous avons activé AppArmor et utilisé un profil personnalisé qui restreint l'accès aux seuls répertoires de données de l'application.

Pour un guide complet sur ces étapes de durcissement, consultez Durcissement des conteneurs Docker pour l'hébergement multi-locataire : un guide d'isolation étape par étape.

Le point de vue divergent : Parfois, vous avez besoin de machines virtuelles

Quoi qu'on fasse, les conteneurs partagent le noyau de l'hôte. Une vulnérabilité du noyau peut briser toute isolation à la fois. C'est pourquoi de nombreuses plates-formes soucieuses de la sécurité exécutent des conteneurs à l'intérieur de machines virtuelles légères — chaque locataire a son propre noyau. Cela ajoute de la surcharge mais offre une frontière au niveau matériel que les conteneurs seuls ne peuvent pas fournir. Si vos locataires traitent des données de carte de crédit ou des dossiers médicaux, une approche hybride (conteneurs dans des machines virtuelles) peut être la bonne solution. Ne supposez pas que l'isolation des conteneurs est suffisante pour votre modèle de menace ; évaluez la sensibilité des données et les exigences réglementaires.

Pour une comparaison plus approfondie des niveaux d'isolation, lisez Conception d'une architecture Docker multi-locataire : choisir le bon niveau d'isolation.

Conclusion : L'isolation est une pile, pas un interrupteur

La correction n'était pas un changement unique — c'était une superposition : isolation réseau, privilèges limités, systèmes de fichiers en lecture seule et filtrage des appels système. Même ainsi, nous avons accepté que l'isolation parfaite est impossible avec des conteneurs partageant le noyau. Pour nos locataires les plus sensibles à la sécurité, nous les avons déplacés vers des hôtes dédiés. La leçon : ne faites confiance à aucune valeur par défaut. Auditez votre configuration Docker comme si une brèche avait déjà eu lieu. Le moment de verrouiller est avant la fuite, pas après.

Sources (5)