Les cas d’utilisations de Docker sont nombreux et facilitent grandement la vie des développeurs quand l’outil est maîtrisé et que les images utilisées sont stables. Il arrive cependant de perdre un temps considérable sur des problèmes propres à l’outil.

Travaillant beaucoup avec Jenkins et les pipelines Groovy ces dernières semaines, j’utilise Docker pour faciliter mon workflow afin de travailler localement et de ne pas interférer avec le Jenkins utilisé par l’équipe. Les jobs en cours de développement sont des pipelines multi-branches pour des projets front Angular.

Une étape évidente lors de l’intégration continue d’un projet Angular est la phase de tests. La plupart du temps les tests Angular, via la commande ng test seront exécutés avec Karma, qui lui même se base sur le navigateur Chrome, plus précisément Chrome avec l’option --headless pour ne pas avoir d’interface graphique.

Dockerfile pour Jenkins et Angular

Voici à quoi pourrait resembler un Dockerfile minimaliste pour faire tourner une intégration continue d’Angular sous Jenkins :

FROM jenkins/jenkins:lts

LABEL maintainer="Antoine Descamps <antoine.descamps@ineat-conseil.fr>"

USER root

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - \
    && apt-get update \
    && apt-get install -y nodejs

# Install Google Chrome
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update && apt-get install -y google-chrome-stable

Précision : l’utilisateur « root » est explicitement sélectionné car à la fin du Dockerfile de Jenkins (https://github.com/jenkinsci/docker/blob/master/Dockerfile#L73) c’est l’utilisateur par défaut qui est utilisé. Sans ça il serait impossible d’installer des paquets additionnels.

Une fois l’image construite (docker build -t myproject/jenkins:latest .) et le conteneur lancé (docker run -p 8080:8080 myproject/jenkins) il ne reste plus qu’à lancer le job de tests de Jenkins. Evidemment ça ne fonctionne pas sinon l’article n’aurait pas lieu d’être !

Première erreur : Operation not permitted

Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permitted

Quand dans une même phrase on retrouve les notions de « namespace », « PID » et « Network » on sent bien que c’est une erreur système bas niveau.

Après un bon moment à parcourir quelques issues sur Github les solutions revenant le plus souvent sont les suivantes :

  1. Lancer Docker avec l’option --privileged
  2. Lancer Docker avec l’option --cap-add=SYS_ADMIN
  3. Configurer Karma pour lancer Chrome avec l’option --no-sandbox

Docker avec l’option « –privileged »

Par défaut, un conteneur Docker a des droits restreints : il ne peut pas accéder à son hôte ni changer les paramètres de son kernel.

L’option --privileged enlève toute cette sécurité, ce qui est évidemment une mauvaise idée.

By default, Docker containers are “unprivileged” and cannot, for example, run a Docker daemon inside a Docker container. This is because by default a container is not allowed to access any devices, but a “privileged” container is given access to all devices (see the documentation on cgroups devices).

When the operator executes docker run –privileged, Docker will enable access to all devices on the host as well as set some configuration in AppArmor or SELinux to allow the container nearly all the same access to the host as processes running outside containers on the host. Additional information about running with –privileged is available on the Docker Blog.

Si vous voulez avoir un exemple concret de pourquoi c’est une mauvaise idée ce post Stackoverflow en est un bon exemple.

Docker avec l’option « –cap-add=SYS_ADMIN »

« cap-add » et « cap-drop » sont des options permettant d’ajouter ou d’enlever des « capacités » au Kernel Linux. On peut voir les « capacités » comme des « flags » correspondants à des droits. Si vous voulez en savoir plus sur les « capacités » Linux, le man 7 capabilities est bien documenté. On retrouve également la liste exhaustive dans la section Runtime privileges and Linux capabilities de la documentation de Docker.

Dans notre cas, c’est la capacité « SYS_ADMIN » qui nous intéresse plus particulièrement :

Perform a range of system administration operations

Même si les droits sont moins importants qu’avec l’option --privileged il n’en restent pas moins dangereux, pour les mêmes raisons évoquées précédemment.

Configurer Karma pour lancer Chrome avec l’option « –no-sandbox »

Par défaut, tous les processus de Chrome (ou plus précisément Chromium) sont exécutés dans une « sandbox ». La sandbox offre un environnement fermé avec très peu de possibilités d’exécuter du code malicieux.

La FAQ de Chromium est claire et concise à ce sujet et si vous voulez comprendre comment est implémenté le mode sandbox de Chromium sous Linux la documentation officielle est également très claire.

Si le mode sandbox est désactivé, deux problèmes se posent :

  1. Problème de sécurité : bien moindre que d’exécuter un conteneur Docker avec tous les privilèges, mais il devient possible de logger les touches pressées, de prendre des captures d’écran, d’émuler le clavier, d’accéder à X11 et j’en passe ;
  2. Configuration des projets à modifier : pour que Karma exécute Chrome sans le mode sandbox, il faut modifier sa configuration https://docs.travis-ci.com/user/chrome#karma-chrome-launcher.

Le premier problème n’en est pas réellement un puisque Chrome ne sera utilisé que pour exécuter des tests JavaScript qui sont à priori sans danger pour le système. Mais quitte à faire les choses correctement, autant aller jusqu’au bout de la démarche.

Le second problème est plus contraignant puisqu’il faut modifier la configuration des projets pour s’adapter à l’environnement d’intégration continue.

Seccomp à la rescousse !

Seccomp, pour « Secure computing mode » est une fonctionnalité du kernel Linux permettant de restreindre les appels système d’un processus.

Lorsque vous exécutez Docker, un profil par défaut est chargé avec la liste des appels système possibles. L’idéal serait de modifier ce profil pour exécuter Chrome en ne lui attribuant que les options de sécurités requises et rien de plus.

Parvenir à trouver tous les appels système réalisés par Chrome et à configurer seccomp correctement n’est pas chose aisée. Heureusement, Jessie Frazelle (ancienne employée de Docker) l’a fait pour nous. Le profil se trouve ici sur son Github et la démarche pour y arriver sur son blog : How to use the new Docker Seccomp profiles.

Une fois le profil téléchargé il faut utiliser l’option --security-opt :

docker run -p 8080:8080 --security-option seccomp=chrome.json myproject/jenkins

Cette fois-ci, vous vous dites que c’est la bonne et que vous allez enfin pouvoir exécuter Karma avec Chrome headless.

Seconde erreur : Chrome ne peut être lancé en root

Après la résolution laborieuse du problème précédent on s’attend à enfin pouvoir lancer Chrome headless dans Docker. C’est sans compter sur une nouvelle erreur :

Running as root without –no-sandbox is not supported. See https://crbug.com/638180.

Plus précisément, Chrome, avec l’option --headless ne peux pas être exécuté avec l’utilisateur « root » si l’option --no-sandbox n’est pas appliquée.

Cette erreur est bien plus simple à traiter que la précédente. Il suffit simplement de suivre le message d’erreur est d’utiliser un autre utilisateur que « root » pour exécuter Chrome.

Bien souvent, dans une image Docker, tout se fait avec l’utilisateur « root ». Il suffit simplement de sélectionner un autre utilisateur à la fin du Dockerfile pour que celui-ci devienne l’utilisateur par défaut :

FROM jenkins/jenkins:lts

LABEL maintainer="Antoine Descamps <antoine.descamps@ineat-conseil.fr>"

USER root

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - \
    && apt-get update \
    && apt-get install -y nodejs

# Install Google Chrome
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update && apt-get install -y google-chrome-stable

USER jenkins

Dans le cas de notre Dockerfile l’utilisateur « jenkins » existe déjà, il a été défini dans l’image jenkins sur laquelle nous nous basons.

Plus de surprise ou de nouvelle erreur cachée. Une fois l’image reconstruite il est maintenant possible pour Jenkins d’exécuter les tests avec Karma dans un conteneur Docker.

Conclusion

Pour conclure voici le Dockerfile entier et fonctionnel pour exécuter Chrome headless, dans Jenkins avec Karma :

FROM jenkins/jenkins:lts

LABEL maintainer="Antoine Descamps <antoine.descamps@ineat-conseil.fr>"

USER root

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - \
    && apt-get update \
    && apt-get install -y nodejs

# Install Google Chrome
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update && apt-get install -y google-chrome-stable

USER jenkins

En plus du Dockerfile vous aurez besoin du profil seccomp personnalisé pour Chrome : https://github.com/jessfraz/dotfiles/blob/master/etc/docker/seccomp/chrome.json

Il faut d’abord construire l’image (la commande est exécutée au même endroit que le Dockerfile) :

docker build -t myproject/jenkins:latest .

Une fois celle-ci construite vous pouvez la lancer avec la commande suivante (notez l’argument de sécurité) :

docker run -p 8080:8080 --security-option seccomp=chrome.json myproject/jenkins

Jenkins est maintenant accessible à l’adresse suivante : http://localhost:8080