Exécutez-vous vos conteneurs en tant qu'utilisateur root ou en tant qu'utilisateur standard ? Cette question est moins simple qu'il n'y paraît. Vous pourriez être tenté de répondre trop rapidement. Le modèle de menace est-il vraiment clair dans votre esprit ? Je soupçonne que ce n'est peut-être pas le cas. Cet article devrait clarifier les choses.

Avant de pouvoir répondre à la question ci-dessus, vous devez déterminer si nous parlons du moteur de conteneur (Podman, Docker, CRI-O, containerd, etc.), du processus à l'intérieur du conteneur (apache, postgresql, mysql, etc.) ou de l'identifiant du processus sur lequel le conteneur est mappé (les trois peuvent être différents). À première vue, cela peut ne pas être évident. Le moteur de conteneur ou son sous-processus dans les conteneurs peuvent être exécutés avec pratiquement n'importe quel utilisateur.

Containers illustration Vous souhaitez en faire plus avec les images UBI (Universal Base Images) de Red Hat ?

Comprendre le mode root

Avec l'arrivée de Podman, les conteneurs rootless ou sans root sont devenus une véritable possibilité. Étant donné que Podman crée des conteneurs en tant que sous-processus directs de lui-même, il est facile de démontrer qu'il existe quatre options possibles. Mais le mode rootless, qu'est-ce que c'est ? Pour comprendre le mode rootless, vous devez comprendre le mode root à l'intérieur d'un conteneur. Et pour comprendre le mode root à l'intérieur d'un conteneur, vous devez comprendre le mode root à l'extérieur d'un conteneur. C'est clair ?

Le tableau suivant montre le mode root à l'intérieur et à l'extérieur du conteneur (merci à Vincent Batts d'avoir figé ces concepts dans mon esprit lors de la conférence DevConf.us 2019). Avec ce cadre, vous devriez commencer à comprendre le mode rootless :

 

Table 1: Root inside and outside the container showing how users appear to the system

Soulignons quelques points intéressants à propos de l'exemple ci-dessus.Tout d'abord, les options de ligne de commande sont -i (interactive) -t (terminal) et -u (user). Combinées, ces options vous donnent un terminal interactif à l'intérieur du conteneur, et spécifient que le processus conteneurisé doit s'exécuter en tant qu'utilisateur sync. Vous pouvez approfondir vos recherches en saisissant la commande « man podman-run ». 

Deuxièmement, faites très attention à $, qui implique que notre shell s'exécute en tant qu'utilisateur standard, et à #, qui implique que notre shell s'exécute en tant qu'utilisateur root. Il s'agit de traditions Unix qui aideront à expliquer le mode root à l'intérieur et à l'extérieur du conteneur. 

Troisièmement, dans l'exemple ci-dessus, Podman est, par définition, en dehors du conteneur et s'exécute en tant qu'utilisateur root ou standard (fatherlinux), tandis qu'à l'intérieur du conteneur, bash s'exécute en tant qu'utilisateur root ou standard (sync). Les utilisateurs du fichier /etc/passwd dans l'hôte du conteneur sont utilisés pour Podman, tandis que les utilisateurs du fichier /etc/passwd dans l'image du conteneur sont utilisés dans le conteneur en cours d'exécution. Autrement dit, ces deux fichiers passwd impliquent que nous pourrions avoir deux ensembles d'utilisateurs : l'un à l'extérieur du conteneur et l'autre à l'intérieur du conteneur. C'est exactement ce que facilitent les espaces de noms d'utilisateurs.

Espaces de noms d'utilisateurs

Lorsque le noyau crée un processus en cours d'exécution à l'intérieur d'un conteneur, l'espace de noms d'utilisateur mappe l'identifiant de l'utilisateur des processus conteneurisés sur un autre identifiant d'utilisateur à l'extérieur du conteneur. Prenons un exemple :

 

Table 2: Showing user IDs and username inside and outside container

Les options de ligne de commande sont -i (interactive) -d (detach) et -u (user). Combinées, ces options exécutent le conteneur en arrière-plan et spécifient que le processus conteneurisé doit s'exécuter en tant qu'utilisateur sync. Là encore, vous pouvez approfondir vos recherches en saisissant la commande « man podman-run ». 

Podman nous fournit également une sous-commande très intéressante appelée top qui nous permet de mapper l'utilisateur dans l'hôte du conteneur sur l'utilisateur dans le conteneur en cours d'exécution. L'exemple ci-dessus montre que lorsque nous exécutons un conteneur en tant qu'utilisateur root, nous mappons l'utilisateur sync (uid 5) dans le conteneur sur l'utilisateur sync (uid 5) dans l'hôte du conteneur sous-jacent. Cela signifie que si un processus sort de ce conteneur, il peut s'exécuter avec les privilèges de l'utilisateur sync réel. 

D'autre part, lorsque nous exécutons exactement le même conteneur en tant qu'un utilisateur standard (fatherlinux), l'utilisateur sync (uid 5) dans le conteneur en cours d'exécution est mappé sur l'uid 100004 dans l'hôte du conteneur sous-jacent. Attendez ! Pourquoi l'utilisateur sync (uid 5) n'est pas mappé sur fatherlinux (uid 1000) ?

Pour faire court, avec les noyaux plus récents et les nouveaux paquets shadow-utils (useradd, passwd, etc.), chaque nouvel utilisateur se voit attribuer une plage d'identifiants utilisateur à sa disposition. Traditionnellement, sur un système Unix, chaque utilisateur n'avait qu'un seul identifiant. Chaque utilisateur peut désormais avoir à sa disposition des milliers d'identifiants à utiliser à l'intérieur de conteneurs. 

C'est utile lorsqu'un conteneur utilise plusieurs utilisateurs, par exemple pour exécuter Apache et MySQL dans un seul conteneur ou pod, ou pour exécuter un conteneur sidecar avec un agent qui s'exécute en tant qu'un autre utilisateur. Alors, d'où vient ce mappage ? De deux fichiers,/etc/subuid et /etc/subgid. Les entrées sont créées dans ces fichiers lorsque des utilisateurs sont ajoutés, via la commande usermod ou manuellement par un administrateur système.

Mieux comprendre les identifiants d'utilisateurs

Voici un exemple d'entrées sur mon système. Avec les entrées suivantes, l'utilisateur Fatherlinux peut mapper jusqu'à 65 535 ID utilisateur dans des conteneurs avec des ID utilisateur réels sur le système à partir de 100 000. Par défaut, shadow-utils (useradd, passwd, etc.) cette plage d'ID utilisateur est réservée à un seul utilisateur. La commande useradd réservera la plage suivante pour le prochain utilisateur. Dans cet exemple, il s'agit de l'utilisateur fred, à partir de l'ID utilisateur 165536 :

cat /etc/subuid Fatherlinux:100000:65536 fred:165536:65536 cat /etc/subgid Fatherlinux:100000:65536 fred:165536:65536

Vous pouvez également voir cette carte depuis l'intérieur d'un conteneur :

 

Table 3: More on users inside and outside the container

Notez que lorsque Podman est exécuté en tant que root, la plage d'ID utilisateur complète est disponible dans le conteneur (4294967295 == 32 bits). Cependant, lorsque Podman est exécuté en tant que Fatherlinux, il mappe root à l'intérieur du conteneur vers l'utilisateur Fatherlinux (1000), et l'utilisateur sync (uid 5) vers un UID compris entre 100 000 et 165 535. 

Il s'agit d'une excellente fonctionnalité de sécurité, car le moteur de conteneur et le processus en conteneur à l'intérieur du conteneur en cours d'exécution s'exécutent tous deux en tant qu'utilisateurs différents et sans privilèges. L'ensemble d'ID utilisateur compris entre 100 000 et 165 535 ne dispose d'aucun privilège spécial sur le système, même en tant qu'utilisateur Fatherlinux (1000). Cela signifie que si un processus dans le conteneur éclate, il sera strictement limité sur l'hôte du conteneur. 

Une autre question qui se pose est la suivante : le système peut-il manquer d'UID lorsque vous ajoutez un groupe d'utilisateurs ? The short answer is, yes. Cependant, cela est peu probable étant donné que les UID sont représentés par un nombre 32 avec 4 milliards d'UID. Cela signifie que vous pouvez ajouter jusqu'à 65 535 utilisateurs à un système (4294967295 divisé par 65 535). Cela devrait suffire dans la plupart des cas d'utilisation.

Examinons une dernière nuance des conteneurs sans racine. Le fichier /etc/subuid est utilisé pour mapper l'utilisateur à l'intérieur du conteneur avec un utilisateur à l'extérieur du conteneur, mais l'utilisateur (fatherlinux dans l'exemple ci-dessous) doit être défini dans l'image du conteneur, sinon Podman ne peut pas démarrer le conteneur :

podman run --user Fatherlinux -it ubi8 bash

Résultat :

impossible de trouver l'utilisateur Fatherlinux : aucune entrée correspondante dans le fichier passwd

Vous devez spécifier un ID utilisateur dans le conteneur qui existe dans le fichier /etc/passwd à l'intérieur de l'image de conteneur. Il s'agit d'un autre exemple de la manière dont les conteneurs sont intrinsèquement liés au système d'exploitation au sein du conteneur et sont séparés du système d'exploitation des hôtes du conteneur. Containers are Linux.

Défense des conteneurs en profondeur

Ce concept n'est pas facile à comprendre avec le démon docker en raison du modèle client-serveur. Avec le modèle de serveur client docker, nous pouvons exécuter un conteneur en tant qu'utilisateur root même lorsque nous exécutons la commande en tant qu'utilisateur normal. En effet, le démon docker s'exécute en tant que root et dispose donc de tous les privilèges de root. Cela devrait être beaucoup plus clair maintenant. À titre d'exemple, exécutez les commandes suivantes :

 

Table 4: Commands to check user IDs

Pour vous assurer qu'un utilisateur exécutant un conteneur n'obtient pas un accès root à votre hôte, vous devez exécuter le moteur de conteneur et le processus conteneurisé en tant qu'utilisateur non root. Cela fournit plusieurs couches de sécurité entre le service (httpd, MySQL, etc.) et les ressources privilégiées dans le système d'exploitation. L'exécution du moteur de conteneur en tant qu'utilisateur non root constitue une couche de défense, tandis que l'exécution du processus dans le conteneur en tant qu'utilisateur non root différent offre une couche de défense supplémentaire. 

Dan Walsh a fait un excellent travail en explorant ce sujet de manière plus approfondie dans cet article : Running Rootless Podman as a non-root User. À un niveau élevé, un moteur de conteneur sans racine comme Podman vous permet de l'exécuter en tant que compte d'utilisateur. Ensuite, à l'intérieur du conteneur, vous pouvez utiliser un ensemble virtuel d'utilisateurs qui sont mappés à un ensemble d'ID utilisateur contrôlés uniquement par votre compte pour les processus en conteneur. 

Vous devez maintenant mieux comprendre les pouvoirs de root à l'intérieur et à l'extérieur du conteneur.


À propos de l'auteur

At Red Hat, Scott McCarty is Senior Principal Product Manager for RHEL Server, arguably the largest open source software business in the world. Focus areas include cloud, containers, workload expansion, and automation. Working closely with customers, partners, engineering teams, sales, marketing, other product teams, and even in the community, he combines personal experience with customer and partner feedback to enhance and tailor strategic capabilities in Red Hat Enterprise Linux.

McCarty is a social media start-up veteran, an e-commerce old timer, and a weathered government research technologist, with experience across a variety of companies and organizations, from seven person startups to 20,000 employee technology companies. This has culminated in a unique perspective on open source software development, delivery, and maintenance.

Read full bio