Conteneurs Docker privilégiés : en avez-vous vraiment besoin ?
5 novembre 2020
0 minutes de lectureCette semaine, en testant Podman, je me suis retrouvé plongé dans une enquête sur les raisons pour lesquelles l’exécution d’un certain conteneur en mode rootless nécessitait l’option --privileged. À juste titre, mon collègue Eric Smalling m’a demandé pourquoi cette option était nécessaire.
En définitive, --privileged signifie qu’on accorde tous les privilèges. Et même si vous pensez que cela n’a pas beaucoup d’importance en mode rootless, cela remet tout de même en cause les principes du moindre privilège et du zéro trust. Même en mode rootless, il est toujours recommandé de bien comprendre les besoins exacts de votre conteneur et de ne lui accorder que les permissions minimales.
Même si cette option n’accorde pas réellement plus de privilèges aux processus exécutés en mode rootless que ceux dont dispose l’utilisateur, je me suis demandé quelles capacités ce processus nécessitait réellement pour qu’on recommande cette configuration. Dans un bel esprit d’exploration, j’ai enfilé mon équipement de spéléologie et me suis aventuré au cœur des ténèbres.
Allons-y
Commençons par voir ce que signifie exécuter des conteneurs en mode rootless. Tout repose sur la magie des espaces de noms utilisateur du noyau Linux, qui permettent aux utilisateurs non privilégiés de créer de nouveaux espaces de noms utilisateur. Lorsqu’un utilisateur crée un nouvel espace de noms utilisateur et y entre, il devient root dans le contexte de cet espace de noms et obtient la plupart des privilèges nécessaires au lancement d’un conteneur fonctionnel. Dans les espaces de noms utilisateur, root ne dispose évidemment pas des mêmes privilèges que le root du système. Cela permet toutefois d’exécuter des conteneurs sans privilèges élevés et de réduire considérablement la surface d’attaque potentielle liée aux vulnérabilités.
J’utilise une machine virtuelle dotée d’une installation minimale de CentOS 8, exécutée dans VirtualBox sur mon MacBook. J’ai d’abord installé podman avec dnf :
J’ai ensuite créé un répertoire dans le répertoire personnel de mon utilisateur et y ai généré un certificat auto-signé :
Je veux maintenant utiliser ce certificat pour exécuter un registre Docker local à des fins de test. C’est là que notre aventure commence vraiment. Toute la documentation que j’avais trouvée à ce sujet recommandait d’utiliser l’option --privileged :
Comme on peut le voir dans la commande ci-dessus, nous exécutons l’image de registre étiquetée 2, créons un montage de volume qui lie le répertoire certs du répertoire courant à /certs dans le conteneur, transmettons des variables d’environnement pour configurer le registre et ajoutons allègrement l’option --privileged, qui demande à podman d’exécuter ce conteneur en mode privilégié.
Pour utiliser le registre avec podman, nous devons ensuite ajouter une entrée dans /etc/containers/registries.conf :
L’exécution en mode privilégié fonctionne parfaitement. J’ai donc d’abord voulu voir ce qui se passerait sans privilèges. C’était assez simple : le conteneur démarre, puis plante et redémarre, et ses journaux indiquent qu’il ne peut pas accéder au certificat.
À ce stade, j’ai supposé que le problème était lié aux capacités Linux, car l’une des principales fonctions de l’option --privileged est de permettre au conteneur d’accéder à toutes les capacités fournies par le noyau. Voici ce que nous montre podman lorsqu’on exécute ce conteneur en mode privilégié :
Je me suis alors tourné vers les pages de manuel Linux pour examiner en détail les actions que chaque capacité permet aux processus d’effectuer. Nous pouvons de nouveau utiliser podman pour vérifier les capacités de notre conteneur non privilégié en cours d’exécution :
En comparant cette liste aux pages de manuel Linux, on constate que les capacités effectives en mode non privilégié devraient suffire à permettre au conteneur de lire des fichiers. En fait, cette liste lui accorde probablement plus de capacités que nécessaire, mais nous y reviendrons plus tard.
L’étape suivante consiste à consulter le fichier audit.log sur l’hôte. Et, sans surprise :
Cette entrée nous indique que SELinux a bloqué l’opération de lecture du fichier domain.crt. Sur les systèmes CentOS et RHEL, SELinux est configuré par défaut en mode Enforcing. Nous pouvons le vérifier avec l’outil sestatus :
En mode Enforcing, SELinux étiquette les fichiers et les répertoires pour gérer leur accès. Ces étiquettes influent également sur le comportement des moteurs de conteneurs. Ceux-ci lancent les processus avec l’étiquette container_t et attribuent au conteneur lui-même l’étiquette container_file_t. SELinux veille à ce que les processus n’interagissent qu’avec les fichiers portant ces étiquettes et refuse par défaut l’accès aux fichiers qui se trouvent hors du conteneur. Avec l’option --privileged, les étiquettes sont désactivées et le conteneur s’exécute avec l’étiquette utilisée au démarrage du moteur de conteneurs. Nous pouvons le vérifier en examinant nos conteneurs avec podman. Voici un conteneur privilégié :
Et voici un conteneur exécuté sans privilèges :
En examinant le répertoire certs dans le système de fichiers de l’hôte, on constate qu’il porte l’étiquette user_home_t :
En pratique, cela signifie qu’en mode non privilégié, ces fichiers ne sont pas accessibles au conteneur, même s’ils sont montés dans l’image.
Au lieu d’exécuter notre conteneur avec --privileged, nous avons plusieurs solutions. Nous pouvons d’abord désactiver complètement les étiquettes en ajoutant --security-opts label=disable à la ligne de commande podman. Ce n’est évidemment pas idéal du point de vue de la sécurité. podman et Docker disposent donc tous deux d’un mécanisme permettant de réétiqueter les montages : utilisez l’option Z pour un montage privé ou l’option z si le montage est partagé.
Pour corriger notre conteneur, il suffit donc de l’exécuter ainsi :
Enfin, pour revenir aux capacités, il est toujours préférable d’exécuter les conteneurs avec le moins de capacités possible. Dans ma configuration de test, il est même possible d’exécuter ce conteneur de registre en supprimant toutes les capacités.
Il existe différentes façons de déterminer les capacités nécessaires à votre conteneur. Vous pouvez, par exemple, consulter dans le journal d’audit les capacités que SELinux bloque pendant l’exécution, puis ajouter celles qui sont nécessaires à l’aide de l’argument --cap-add. Le principe du moindre privilège reste toujours la meilleure option ! La morale de cette histoire : ne jetez pas le bébé avec l’eau du bain.
En résumé
Déclarer des conteneurs --privileged, même dans des espaces de noms utilisateur, n’est pas une bonne pratique et va à l’encontre des principes du moindre privilège et du zéro trust. Avant de lancer votre conteneur, déterminez ses besoins réels en vous appuyant sur des outils comme SELinux pour auditer les capacités et permissions demandées par votre image, puis accordez-lui uniquement celles-ci lors de son exécution en production. Vous constaterez souvent que ses besoins sont en réalité très limités.
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.