Le user namespace dans Docker

Avant l’arrivée du user namespace dans docker

Pour rentrer dans le vif du sujet, classiquement, lorsqu’on veut « lancer » un conteneur Docker, on procède comme suit :

docker run –d jenkins

Dès lors, on est en droit de se demander (ou le RSSI vous demandera), à quel utilisateur appartient ce conteneur ? Regardons :

jclegras 14755 64.2  3.6 3033816 147792 ? Sl 19:27 0:06 java -jar /usr/share/jenkins/jenkins.war

Très bien, et maintenant, que se passe-t-il si je veux que le conteneur appartienne à root ?

docker run -u root –d jenkins

Et regardons, encore une fois, le résultat obtenu :

root     14893 91.5  3.6 3023668 148888 ? Sl 19:29 0:05 java -jar /usr/share/jenkins/jenkins.war

Une question s’impose alors d’elle-même: comment le docker engine fait-il pour faire la correspondance entre les users de mes conteneurs, et les users de ma machine hôte, ou autrement dit, comment se fait-il que le premier appartienne à jclegras et le deuxième à root ?

Pour le premier exemple, il faut regarder d’un peu plus près le Dockerfile de Jenkins.

Les lignes qui nous intéressent sont :

  ARG user=jenkins
  ARG group=jenkins
  ARG uid=1000
  ARG gid=1000

  # Jenkins is run with user `jenkins`, uid = 1000
  # If you bind mount a volume from the host or a data container, 
  # ensure you use the same uid
  RUN groupadd -g ${gid} ${group} 
      && useradd -d "$JENKINS_HOME" -u ${uid} -g ${gid} -m -s /bin/bash ${user}
  [...]
  USER ${user}

Et voici ce que ça raconte : on créer un user et un group jenkins avec un uid et un gid égales à 1000 puis on dit que, par défaut, en l’absence de user explicite (l’option -u que nous avons fourni plus haut), le conteneur s’exécutera avec les droits de l’utilisateur jenkins.

Mais alors, comment se fait-il qu’un ps m’affirme que le conteneur appartient à jclegras ?

A dire vrai, avant la 1.10, Docker mappait les users (ou plus exactement les uid/gid) selon l’hôte:

root@jclegras-VirtualBox:/home/jclegras# id jclegras
uid=1000(jclegras) gid=1000(jclegras) ...

C’est ainsi qu’on aperçoit le côté tricky de la chose : à l’intérieur du conteneur, par défaut, je suis le user jenkins mais à l’extérieur, je suis mappé sur un user qui correspond à jenkins, c’est à dire à quelqu’un possédant ses identifiants :

uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)

Autrement dit, l’utilisateur jenkins est en réalité l’utilisateur jclegras sur la machine hôte !

Par extension, lorsque j’ai lancé mon deuxième conteneur en tant que root, l’utilisateur root du conteneur était en réalité l’utilisateur correspondant aux identifiants (uid/gid) de root :

uid=0(root) gid=0(root) groups=0(root)

Et je ne vous apprends rien en disant que ce sont exactement les même identifiants que le user root de la machine hôte. Le root du conteneur est donc mappé sur le root de l’hôte !

C’est pourquoi, dans les bonnes pratiques de Docker, il est conseillé de lancer vos conteneurs sous un autre utilisateur que root (ce qu’ont bien compris les concepteurs de l’image Jenkins).

Seulement voilà, même en lançant le conteneur avec le user jenkins :

docker run –d jenkins

Rien ne m’empêche par la suite de rentrer à l’intérieur avec l’utilisateur root (il y a toujours un utilisateur root disponible dans les conteneurs) :

docker exec -u root -it [mon_conteneur_jenkins] bash

Vous me direz que ce n’est rien, après tout, tout ce que je peux faire dans le conteneur reste dans le conteneur, et c’est le but d’un tel outil. Mais que se passe-t-il lorsque nous commençons à monter des volumes ? :

docker run –v $(pwd)/jenkins_home:/var/jenkins_home –d jenkins

Ou plus généralement :

docker run –v /répertoire_critique:/opt/dest –d jenkins


Et bien vous imaginez dès lors les conséquences d’une prise de contrôle de la part d’un utilisateur malveillant sur, par exemple, une application web ayant au moins un volume partagé : on peut tout supprimer. Et si mon “répertoire_critique” contient d’autres fichiers que ceux exigés par l’application web, je peux aussi les modifier et mettre en péril, dans le pire des cas, l’ensemble de mon système hôte (pas génial si vous êtes en prod…).

L’idéal serait d’avoir un système permettant de cloisonner ces utilisateurs dans le conteneur afin que, même si je suis root à l’intérieur du conteneur (et que je puisse donc faire tout ce que bon me semble), je ne puisse pas mettre en danger le système hôte.
De la même façon, même si j’utilise un utilisateur custom dans mon conteneur, je voudrais ne pas correspondre à un user random sur le système hôte (même si dans les faits, pour éviter que jenkins corresponde à jclegras, on fait en sorte d’attribuer des uid et des gid plus élevés afin de faire correspondre jenkins au “vrai” user jenkins sur l’hôte créé pour l’occasion).
Cependant, et vous vous en doutez, il faut aller bidouiller les différents Dockerfiles de toutes ces images officielles et procéder nous-même aux modifications qui vont bien (tout en répercutant sur le système hôte) et en espérant que mon user jenkins ne se voit pas, un beau jour, attribuer des privilèges qu’il ne mérite pas (tiens, pourquoi jenkins est dans les sudoers d’un seul coup ?)

Bref, c’est ici que le user namespace finalement intégré au Docker engine (bien que présent depuis quelque temps dans le noyau linux) arrive à la rescousse. Succinctement, voici ce que ça permet de faire, mapper (dynamiquement) mes users comme suit :

root -> un user_non_privilégié
jenkins -> un autre user_non_privilégié
...

 

Après l’arrivée du user namespace dans docker

Concrètement, voici comment ça marche : https://blog.yadutaf.fr/2016/04/14/docker-for-your-users-introducing-user-namespace/

Je ne sais pas si c’est très clair, mais essayons de “singer” ce tuto en partant d’un besoin réel :

Je souhaite que tous mes conteneurs s’exécutant sous root soient mappés sur le user dockremap ;
Je souhaite également que les conteneurs utilisant un utilisateur personnalisé d’UID 1000 et appartenant à un groupe de GID 1000 soient mappés sur l’utilisateur et le groupe dockremap-user ;
Finalement, je souhaite que les users/groupes dockremap et dockremap-user possèdent le moins de droit possible.
Commençons par l’étape de création des utilisateurs et des groupes mentionnés :

    groupadd -g 500000 dockremap && 
      groupadd -g 501000 dockremap-user && 
      useradd -u 500000 -g dockremap -s /bin/false dockremap && 
      useradd -u 501000 -g dockremap-user -s /bin/false dockremap-user

Ensuite, ajoutons une liste d’utilisateurs “subalternes” et une liste de groupes “subalternes” en utilisant comme point de référence, resp., l’utilisateur et le groupe dockremap :

  echo "dockremap:500000:65536" >> /etc/subuid && 
    echo "dockremap:500000:65536" >>/etc/subgid

Finalement, ajoutons l’option magique –userns-remap pour activer le user namespace dans les conteneurs (passons par le fichier de conf du démon docker pour l’occasion) :

  {
    "userns-remap": "default"
  }

On remarquera que default est un alias de dockremap : en fait, quand on met default, le démon docker crée (si ce n’est pas déjà fait), un utilisateur et un groupe dockremap avec un “numerical subordinate user/group ID” positionné à un offset correspondant à ce qui était déjà présent dans les fichiers /etc/subuid et /etc/subgid lors de la création (on aurait donc pu se passer des étapes précédentes, mais on y gagne au moins deux choses : nous avons choisi nos propres ID [500000 dans les deux cas] et on sait donc le faire manuellement).

Pour finir, redémarrons le démon docker :

systemctl daemon-reload && systemctl restart docker

Mission accomplie, nos users sont bien mappés comme nous le voulions !

En fait, en s’appuyant sur les utilisateur et groupe de référence dockremap fixés tous deux à 500000 et sur la configuration des “subordinate files“(les /etc/sub*), le mapping sera comme suit:

  | User Host ID   |  User Container ID|
  |----------------|:------------------|
  | 500000         | 0                 | (root)
  | ...            | ...               |
  | 501000         | 1000              | (jenkins, ...)
  | ...            | ...               |
  | 565536         | 65536             |

On peut donc dès lors se contenter d’utiliser root dans tous nos conteneurs puisque ce dernier sera automatiquement converti en dockremap sur l’hôte, tout en gardant les avantages d’être root, et donc tout puissant, à l’intérieur de nos conteneurs.

 

Bonus

Cette section est créée en guise d’avertissement.

Avant, lorsque nous étions root dans nos conteneurs, on pouvait monter très simplement nos volumes partagés (j’entends par là : entre le système hôte et le conteneur cible) :

docker run -v /mon_repertoire:/opt/webapp/mon_repertoire -d [mon_image]

Et je pouvais faire ce que je voulais sur les fichiers contenus dans mon_repertoire (les modifier, les supprimer…). En effet, rappelez-vous, root du conteneur était mappé sur root de l’hôte et par conséquent, il avait tous les droits. Comment faire dès lors pour pouvoir modifier les fichiers contenus dans le répertoire lorsque j’active le user namespace et que mon user root est mappé sur dockremap ?

Et bien c’est très simple, il vous faudra attribuer les droits qui vont bien aux fichiers en question :

chown -R dockremap:dockremap /mon_repertoire/

Voici une démo qui montre que ça fonctionne pas trop mal :

root@jclegras-VirtualBox:/home/jclegras/test_docker# ls -l test_docker/
total 8
-rw-r--r-- 1 dockremap      dockremap       0 sept.  4 01:01 dummy_dockremap
-rw-rw-r-- 1 dockremap-user dockremap-user  0 sept.  4 01:00 dummy_dockremap-user
-rw-rw-r-- 1 jclegras       jclegras       12 sept.  3 16:29 dummy_jclegras
-rw-rw-r-- 1 jenkins        jenkins        12 sept.  2 00:59 dummy_jenkins
-rw-rw-r-- 1 root           root            0 sept.  2 00:28 dummy_root

root@jclegras-VirtualBox:/home/jclegras/test_docker# docker run -v $(pwd)/test_docker:/var/jenkins_home/test_docker -d jenkins
070003aa39b81a53ffe6b01f44986922058f1585d8da285f981cf3b673b9d734

root@jclegras-VirtualBox:/home/jclegras/test_docker# docker exec -it 070 bash

jenkins@070003aa39b8:/$ ls -l /var/jenkins_home/test_docker/
total 8
-rw-r--r-- 1 root    root     0 Sep  3 23:01 dummy_dockremap
-rw-rw-r-- 1 jenkins jenkins  0 Sep  3 23:00 dummy_dockremap-user
-rw-rw-r-- 1 nobody  nogroup 12 Sep  3 14:29 dummy_jclegras
-rw-rw-r-- 1 nobody  nogroup 12 Sep  1 22:59 dummy_jenkins
-rw-rw-r-- 1 nobody  nogroup  0 Sep  1 22:28 dummy_root

jenkins@070003aa39b8:/$ whoami
jenkins

jenkins@070003aa39b8:/$ id jenkins
uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)

jenkins@070003aa39b8:/$ echo "hello world" /var/jenkins_home/test_docker/dummy_dockremap    
bash: /var/jenkins_home/test_docker/dummy_dockremap: Permission denied

jenkins@070003aa39b8:/$ echo "hello world" >> /var/jenkins_home/test_docker/dummy_dockremap-user

jenkins@070003aa39b8:/$ echo "hello world" >> /var/jenkins_home/test_docker/dummy_dockremap-jclegras
bash: /var/jenkins_home/test_docker/dummy_dockremap-jclegras: Permission denied

jenkins@070003aa39b8:/$ echo "hello world" >> /var/jenkins_home/test_docker/dummy_dockremap-jenkins 
bash: /var/jenkins_home/test_docker/dummy_dockremap-jenkins: Permission denied

jenkins@070003aa39b8:/$ echo "hello world" >> /var/jenkins_home/test_docker/dummy_dockremap-root   
bash: /var/jenkins_home/test_docker/dummy_dockremap-root: Permission denied

En utilisant root dans mon conteneur :

root@jclegras-VirtualBox:/home/jclegras/test_docker# docker exec -u root -it 070 bash

root@070003aa39b8:/# ls -l /var/jenkins_home/test_docker/                                                                                                                                                                           
total 12
-rw-r--r-- 1 root    root     0 Sep  3 23:01 dummy_dockremap
-rw-rw-r-- 1 jenkins jenkins 12 Sep  3 23:07 dummy_dockremap-user
-rw-rw-r-- 1 nobody  nogroup 12 Sep  3 14:29 dummy_jclegras
-rw-rw-r-- 1 nobody  nogroup 12 Sep  1 22:59 dummy_jenkins
-rw-rw-r-- 1 nobody  nogroup  0 Sep  1 22:28 dummy_root

root@070003aa39b8:/# id `whoami`
uid=0(root) gid=0(root) groups=0(root)

root@070003aa39b8:/# echo "hello world" >> /var/jenkins_home/test_docker/dummy_dockremap

root@070003aa39b8:/# echo "hello world" >> /var/jenkins_home/test_docker/dummy_dockremap-user

root@070003aa39b8:/# echo "hello world" >> /var/jenkins_home/test_docker/dummy_jclegras         
bash: /var/jenkins_home/test_docker/dummy_jclegras: Permission denied

root@070003aa39b8:/# echo "hello world" >> /var/jenkins_home/test_docker/dummy_jenkins 
bash: /var/jenkins_home/test_docker/dummy_root: Permission denied

Et enfin, en regardant la table des processus (avec le conteneur lancé avec le user par défaut, jenkins) :

root@jclegras-VirtualBox:/home/jclegras/test_docker# ps aux | grep dock

dockrem+ 16807  0.0  0.0   1112     4 ?        Ss   01:03   0:00 /bin/tini -- /usr/local/bin/jenkins.sh
dockrem+ 16825  2.3  5.2 3073244 213492 ?      Sl   01:03   0:17 java -jar /usr/share/jenkins/jenkins.war

root@jclegras-VirtualBox:/home/jclegras/test_docker# ps -U dockremap-user
  PID TTY          TIME CMD
  16807 ?        00:00:00 tini
  16825 ?        00:00:17 java

 

Ressources

Le user namespace en long, en large et en travers :

 
Les namespaces du kernel linux, fondations des conteneurs (mais pas que) :

 
Bonnes pratiques Docker :

 
Ansible :

 

Conclusion

J’espère que ce billet vous aura plu et qu’il vous donnera envie de creuser le sujet plus en profondeur (Docker, les namespace linux, les fondations des conteneurs…).

Leave a Comment