Containerization comparé à un process normal
La containerization est en 2025 ultra-populaire, et la manière la plus simple de la présenter une première fois est de la comparer avec une VM comme le fait de nombreuse vidéo youtube peuvent vous guider. C’est une bonne façon d’introduire, tellement bonne que 7 sur les 10 premières vidéos que me propose youtube avec les termes “docker explained” font toutes la comparaison avec un VM. Toutefois, se limiter à cette comparaison manque de profondeur. Aussi, la majorité des exemples trouvés sur le net donnent l’impression de travailler avec un VM, alors qu’un container est un process comme un autre.
Cet article est franglais. J’utiliserai les mots anglais pour process, container, namespace mount, directory et probablement d’autres non listés.
Ceci peut donner l’impression de travailler avec des VMs:
$ podman run --rm -it --name ubuntu docker.io/ubuntu:24.04
$ echo "hello world" # ceci est exécuté dans le container. C'est comme un VM ?
hello world
$ whoami
root # user root du container
$ exit
$ whoami
worming # mon utilisateur sur le host
$ podman run --rm --name ubuntu -d docker.io/ubuntu:24.04 sleep 500
$ ps -aux | grep sleep
worming 7392 0.0 0.0 2696 1424 ? Ss 13:52 0:00 sleep 500 # Ceci est le container avec le pid 7392, avec mon utilisateur
worming 10453 0.0 0.0 6460 3780 pts/2 S+ 13:57 0:00 grep sleep # Ceci est le grep lancé 2 lignes plus hauts
$ podman rm -f ubuntu
$ docker run --rm --name ubuntu -d docker.io/ubuntu:24.04 sleep 500
root 21206 0.0 0.0 2696 1232 ? Ss 14:20 0:00 sleep 500 # Ceci est le container avec le pid 21206, avec l'utilisateur root
worming 21552 0.0 0.0 6460 3780 pts/3 S+ 14:21 0:00 grep sleep # Ceci est le grep lancé 2 lignes plus hauts
Cet article a pour objectif de mieux comprendre qu’est-ce qu’un container du point de vue du système d’exploitation, quels sont les risques de sécurité avec une utilisation naive, et comment minimiser les risques en cas de cyberattaque …
Disclaimer, ma connaissance des containers sur windows m’est restreinte. Tout l’article est à propos des containers linux.
Détruire la comparaison avec un VM⌗
Mais l’image ubuntu utilisée plus haut contient un package manager, c’est une distribution qui s’utilise comme mes VMs !⌗
$ podman run --rm -it --name ubuntu docker.io/ubuntu:24.04
$ apt update
$ apt install curl -y
# [...] success avec un long message
Effectivement, l’image embarque un package manager apt. Mais il n’embarque pas de noyaux. Toutes les commandes éxécutées dans le container emploie des systems calls de votre OS principal. C’est d’ailleurs pour ça que le grep de l’introduction affiche bien la commande sleep du container : c’est le noyaux OS hôte qui l’éxécute.
Mais je peux lancer 2 processes dans le même containers ?⌗
$ podman run --rm -it --name ubuntu -d docker.io/ubuntu:24.04 bash -c "sleep 500 | grep foobar | grep foobar"
$ ps -aux | grep foobar
worming 31667 0.0 0.0 4324 2720 pts/0 Ss+ 14:35 0:00 bash -c sleep 500 | grep foobar | grep foobar # process initial du container
worming 31670 0.0 0.0 3528 1672 pts/0 S+ 14:35 0:00 grep foobar # premier grep pipé du container
worming 31671 0.0 0.0 3528 1660 pts/0 S+ 14:35 0:00 grep foobar # second grep pipé du containe
worming 31707 0.0 0.0 6460 3732 pts/4 S+ 14:35 0:00 grep foobar # la deuxième commande de cet exemple
# Il y a donc bien 3 process lié au container.
C’est exact, mais ils tournent bien tous sur l’hôte. Permettre l’exécution de plusieurs process à l’aide d’un seul démarrage de container contribue à ce sentiment de travailler avec un VM.
Ces 3 process utilisent les mêmes namespaces. Nous en reparlons plus bas.
Mais le container est isolé de l’hôte ?⌗
$ podman run --rm -it --name ubuntu docker.io/ubuntu:24.04 ls /
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
$ podman run --rm -it --name ubuntu docker.io/ubuntu:24.04 ls /home
ubuntu # Ceci n'est pas mon home
Effectivement, comme une VM et par défaut, un container démarre avec son propre système de fichier. Mais l’isolement est fait par un mount namespace. Promis, on explique plus bas.
Mais j’utilise un utilisateur propre au container ?⌗
podman run --rm --name ubuntu docker.io/ubuntu:24.04 bash -c "echo \$UID"
0
echo $UID
1000 # pas me le même user ID
C’est effectivement un autre identifiant d’utilisateur depuis le container. Mais je vous promet que linux utilise le même utilisateur à travers la commande podman, c-à-d le mien. On l’a d’ailleurs vu en introduction que la commande sleep avec podman utilise bien mon utilisateur présent sur l’hôte.
Toutefois, selon l’installation, l’usage est les arguments passés aux commandes docker/podman, celà peut être différent. À des fins de simplifications, j’utilise podman avec une installation par défaut, c-à-d rootless, et j’utilise docker avec une installation par défaut, c-à-d rootfull et en ajoutant mon utilisateur au groupe docker (spoiler alerte, j’ai ajouté mon utilisateur uniquement pour la rédaction de cet article et pour avoir une utilisation proche de ce que j’observe chez mes collègues). Si vous une installation custom, ou bien que vous indiquer à podman quel utilisateur employer, il se peut que le container utilise un autre utilisateur que le vôtre.
Mais c’est quoi un namespace à la fin ?⌗
Un namespace est un isolement qu’offre le noyau linux sur des processes. Il y a différents types, et plus haut, nous avons parlé de namespace pour une monture de système de fichier. Nous avons aussi vu celui des utilisateurs qui attribue des USERID différents selon si l’on observe depuis le container, ou depuis le host. Il en existe d’autre comme celui des PIDs, réseau, IPC etc …
Le moyen le plus rapide pour jouer avec les namespaces est d’utiliser la commande unshare. Par exemple l’exemple ci-dessous lance une session sh avec un nouveau namespace de PIDs :
$ sudo unshare --pid --fork sh # Démarre une session sh qui ne partage pas le même
$ echo $$ # imprime le PID de la session sh en cours.
1
# depuis un autre terminal:
$ ps -aux | grep sh
[...] # pas 1 :)
# quitter la session sh depuis le premier terminal:
$ exit
Vous seriez tenté de lancer
ls
depuis la nouvelle session sh, et verrez les mêmes processes que sur votre hôte. C’est parce que ls utilise le directory /proc pour répertorier tous les processes. Comme la commande que vous avez lancé n’utilise pas de mount namespace, vous voyez donc les processes de l’hôte.
Pourquoi c’est important ?⌗
Ceci implique qu’il est plus facile pour un attaquant qui agit sur le process du container d’accéder au serveur hôte. D’autant plus que docker lance des process avec l’utilisateur root par défaut, un attaqueur peut à l’aide de CVE lancer des commandes arbitraires sur le serveur host sans aucune restriction.
Est-ce que ce genre de CVE existe ? Oui, une petite recherche google en montre celle-ci en 2024. L’utilisation de container ne nous protège pas des vulnérabilités trouvés avec le temps. Nous nous devons de limiter le champ d’action des attaqueurs.
Ne pas utiliser d’image avec un shell⌗
Un attaqueur tentera d’utiliser les outils à sa disposition. Plus nous en retirons, moins il lui sera aisé d’accéder à ce qu’il désire. En retirant le shell, il lui sera beaucoup moins pratique de lancer des commandes ouvertement.
Ne pas utiliser d’image avec package manager⌗
Le shell n’est pas le seul moyen de lancer des nouvelles commandes. C’est pourquoi il est aussi recommandé de ne pas utiliser d’images avec un package manager. Le package manager ainsi qu’une connection ouverte vers un repository de package permet à l’attaquant d’installer ses outils préférés. Autrement dit, construisez des images avec le strict nécessaire pour votre application.
Éviter l’usage du flag privileged⌗
Ce flag permet aux containers d’accéder à des ressources de l’hôte. À vos risques et périls.
Pour docker, sécuriser /var/run/docker.sock⌗
Docker a une architecture client-serveur. Chaque commande docker fait une requête http sur /var/run/docker.sock. Mon installation pour cet article https://docs.docker.com/engine/install/linux-postinstall/, auquel j’observe que beaucoup de mes collègues le font. Parce que “sudo” devant chaque commande docker est ennuyant.(c’est réellement ennuyant, mais j’espère vous convaincre qu’ennuyant est mieux que permissifs)
# ne faites pas ça, on explique plus bas pourquoi
sudo groupadd docker
sudo usermod -aG docker $USER
Si vous pouvez lancer des commandes docker sans sudo
, ça signifie que votre utilisateur peut utiliser /var/run/docker.sock pour interagir avec le backend de docker sans restriction. La commande suivante montre comment créer un container sans la commande docker
, simplement avec curl
, avec le flag privileged. Bien sûr, n’importe lequel vos processes avec votre utilisateur peut démarrer un container. Autrement dit, n’importe lequel de vos process est indirectement root de votre ordinateur.
$ IMAGEID=$(curl --unix-socket /var/run/docker.sock http://localhost/v1.41/containers/create -X POST -s \
-H "Content-Type: application/json" \
-d '{
"Image": "nginx",
"Cmd": ["nginx", "-g", "daemon off;"],
"HostConfig": {
"Privileged": true,
"PortBindings": {
"80/tcp": [{"HostPort": "8080"}]
}
}
}' | jq ".Id" -r)
$ docker inspect $IMAGEID # vous avez un container
Pour empêcher l’accès au backend docker, n’ajoutez pas votre utilisateur au groupe docker.
Une fois cet article rédigé et publié, je me retire de ce groupe avec
sudo gpasswd -d $USER docker
N’hésitez pas à faire un
alias sdocker='sudo docker
Ne donnez pas accés à /var/run/docker.sock à des images non fiables⌗
Vous verrez parfois des documentations vous demandez de partager /var/run/docker.sock via le flag –volume. Même si je reste persuadé que la majorité des images ne font rien de malveillant, sachez que ceci donne indirectement un accés root sur votre host au container.
Chez un client et pour démontrer les risques, j’avais produit une image qui permettait de récupérer discrètement les clés ssh d’un utilisateur peu attentif s’il donnait accés au socket de docker. Vous vous doutez bien que cette image n’a pas été partagée et n’a servi que de démonstration afin de sensibiliser les équipes de développement. J’aurais très bien pu aussi ajouter une backdoor ou bien changer les règles firewalls. Ne faites pas ça, ne faites pas confiance à internet non plus.
Pour podman, n’utilisez pas le podman service.⌗
L’architecture client-serveur de docker est convéniente pour créer des outils. N’importe quel outil peut l’utiliser pour communiquer avec docker. Par exemple, lazydocker n’utilise pas le cli docker, mais directement /var/run/docker.sock. Cette méthode d’IPC(Inter Process Communication) est hautement extensible est aussi l’une des raisons du succés de docker.
Podman, par défaut, n’utilise pas cette architecture. Le podman cli crée lui-même les containers. L’avantage est qu’il n’y a pas besoin de composant supplémentaire (comme le docker backend) pour démarrer des containers.
Toutefois, red hat a compris que cet architecture et moins pratique pour les outils tiers. Ceci m’empêcher d’utiliser lazydocker avec podman. Mais pour satisfaire ce manque, podman propose toutefois d’installer un podman service qui utilise une architecture et un protocole identique à docker. Ce qui signifie que les outils compatibles avec docker sont compatibles avec le podman service
# dans un premier terminal
$ podman system service --time=0 unix:///tmp/podman.sock
# dans un second terminal
$ DOCKER_HOST=unix:///tmp/podman.sock lazydocker
# tadaaa, lazydocker cible mes containers podman
Bref, le podman service est donc un composant architecturale pour étendre podman, identique au backend de docker.
Cependant, si vous avez préalablement sélectionné podman au lieu de docker, cela signifie probablement que vous privilégiez la sécurité face à la convénience, et risquez une faille identique à celle de docker en installant le podman service.
$ IMAGEID=$(curl --unix-socket /var/run/docker.sock http://localhost/v1.41/containers/create -X POST -s \
-H "Content-Type: application/json" \
-d '{
"Image": "nginx",
"Cmd": ["nginx", "-g", "daemon off;"],
"HostConfig": {
"Privileged": true,
"PortBindings": {
"80/tcp": [{"HostPort": "8080"}]
}
}
}' | jq ".Id" -r)
$ podman inspect $IMAGEID
# Même risque que docker. Mais par défaut, les containers podman ne sont pas démarrer avec l'utilisateur root. Un attanquant peut faire moins de dégats.
Utilisez des containers rootless⌗
Comme vu plus tôt, les containers de docker utilisent par défaut l’utilisateur root. C’était d’ailleurs l’un des arguments principaux de podman qui voulait corriger le tir (ou prendre des parts de marché?) en proposant des containers rootless, qui emploie un utilisateur précédemment sélectionné. Depuis docker propose aussi de faire depuis cette page. Si vous vous dirigez vers un usage rootless, je vous propose de privilégier podman. C’était d’ailleurs la recommandation employée dans mon article précédent
Aller plus loin⌗
Encore une fois, ceci n’est qu’un blog et n’a pas vocation a être complet. Si le sujet vous intéresse, je vous conseille l’excellent bouquin Container Security: Fundamental Technology Concepts That Protect Containerized Applications. Les namespaces ne sont pas les seules capacités de sécurité employée par le noyau Linux.
A propos de l'auteur
Je m'appelle Mathieu Scolas. Constatant que peu de blogueurs proposent du contenu personnel, constructif et nouveau, ce blog tente de compléter ce manque. Il me permet de partager des guidelines et exprimer des opinions sur le développement d'application. Dans la volonté d'éveiller les lecteurs sur différents sujets, ma façon est de comparer les bonnes pratiques avec les interprétations populaires, ou de présenter des sujets peu évoqués ailleurs. Ce blog me sert aussi de source pour exprimer au mieux des sujets qui méritent un support pour expliquer convenablement les avantages des pratiques, comment les appliquer correctement, et quelles en sont les origines. Retrouvez-moi sur Linkedin et Github.