Aller au contenu principal

Sécuriser l'utilisation de Docker

C'est un reproche qu'on entend souvent : Docker ce n'est pas secure. C'est vrai, par défaut Les conteneurs Docker s'exécutent avec le compte root, de même que les programmes qui s'exécutent à l'intérieur du conteneur.

Mais il existe des solutions permettant de sécuriser l'utilisation de Docker.

Revenons sur le problème

Le mieux c'est d'utiliser un exemple qui va vous parler.

Par exemple en utilisant le container chrisfosterelli/rootplease il est possible d'ouvrir un shell avec les droits roots sur la machine hote. Donc vous imaginez, si vous utiliser une image docker sans en connaître le contenu. Vous encourez de graves problèmes! Un script malveillant pourrait exploiter cette surface pour accéder à des fichiers sensibles, des images, des certificats, voir pire !

Mais il est important de savoir que le simple fait d'ajouter un user au groupe docker donne des droits root sur la machine hôte à celui-ci.

Je vous propose de voir quelques solutions.

Utilisation de la directive USER

Si vous voulez jouer avec ces exemples je vous conseille dans le faire dans une VM, créé avec vagrant.

En ajoutant simplement la directive USER à vos Dockerfile vous allez restreindre les droits du container :

FROM alpine:3.14.2
RUN mkdir /app
RUN addgroup mongroup && adduser -D -H -s /bin/false -G mongroup monuser
WORKDIR /app
COPY . /app
RUN chown -R monuser:mongroup /app
USER monuser
CMD id && ls -alrt /app

Lançons le build et l'image obtenue

docker build -t test .

docker run --rm test
uid=1000(monuser) gid=1000(mongroup) groups=1000(mongroup)
total 4
-rw-rw-r--    1 monuser  mongroup       204 Oct 25 09:43 Dockerfile
drwxr-xr-x    1 monuser  mongroup        24 Oct 25 09:43 .
drwxr-xr-x    1 root     root             6 Oct 25 09:50 ..

Montons / dans l'image et vérifions si on peut effacer des données dans /etc

docker run -v /:/hostdir -it --rm test:latest sh

/app
/app $ rm -f /hostdir/etc/passwd
rm: can't remove '/hostdir/etc/passwd': Permission denied

Donc cela veut dire que vous devrez monter un répertoire possédant les mêmes UID et GID que le USER du container (1000:1000 par défaut) pour créer/modifier/détruire des données.

On peut outre-passer cette règle en utilisant l'option -u <user> à la commande de lancement du container:

docker run --rm -u root -it test sh
id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Ca ne vous protège donc pas d'une utilisation malveillante d'un utilisateur.

Restriction des capabilities

Le noyau Linux permet de segmenter les privilèges de l’utilisateur root en unités distinctes appelées capabilities. Par exemple, la capabilitie CAP_CHOWN permet à un utilisateur d’apporter des modifications aux UID et aux GID des fichiers.

Par défaut, Docker supprime toutes ces capabilities, à l’exception de celles qui lui sont nécessaire.

Comment obtenir la liste des capabilities de votre container. Il suffit d'utiliser getpcaps. Ajoutons le à notre image :

FROM alpine:3.14.2
RUN mkdir /app && \
    addgroup mongroup && \
    adduser -D -H -s /bin/false -G mongroup monuser && \
    apk --no-cache add libcap
WORKDIR /app
COPY . /app
RUN chown -R monuser:mongroup /app
USER monuser
CMD getpcaps 1
docker build -t test .
docker run --rm test
1: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap=i

Dans le noyau Linux les capabilities commencent toutes par CAP_ (CAP_CHOWN, CAP_NET_ADMIN, CAP_SETUID, CAP_SYSADMIN, ...) Dans docker on les retrouve sans ce préfixe.

Voici donc ce qu'on autorise à une image Docker de faire par défaut:

  • CAP_CHOWN: Avoir la capacité de changer le propriétaire d'un fichier;
  • CAP_DAC_OVERRIDE: Passer outre le contrôle d'accès (Posix ACL);
  • CAP_FSETID: Avoir la capacité d'utiliser chmod sans limitation;
  • CAP_FOWNER: Outrepasser le besoin d'être propriétaire du fichier;
  • CAP_MKNOD: Avoir la capacité d'utiliser des fichiers spéciaux;
  • CAPNETRAW: Avoir la capacité d'utiliser les sockets raw et packet ( snifiing, binding);
  • CAP_SETGID: Avoir la capacité de changer le GID;
  • CAP_SETUID: Avoir la capacité de changer l'UID;
  • CAP_SETFCAP: Avoir la capacité de modifier les capacités d'un fichier;
  • CAP_SETPCAP: Avoir la capacité de modifier les capacités d'un autre processus;
  • CAPNETBIND_SERVICE:, Avoir la capacité d'écouter sur un port inférieur à 1024;
  • CAPSYSCHROOT: Avoir la capacité de faire un change root;
  • CAP_KILL: Avoir la capacité de killer un processus;
  • CAPAUDITWRITE: Avoir la capacité d'écrire des logs Kernels

Pour limites les droits il suffit d'ajouter l'option --- lors du lancement du container.

Reprenons notre exemple ci-dessus cela va nous permettre de vérifier que ces droits sont bien retiré.

docker run --rm \
--cap-drop=chown \
--cap-drop=dac_override \
--cap-drop=fowner \
--cap-drop=fsetid \
--cap-drop=kill \
--cap-drop=setpcap \
--cap-drop=mknod \
--cap-drop=setfcap test
1: cap_setgid,cap_setuid,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_audit_write=i

C'est bien le résultat attendu. Mais tout est configuré au niveau de la ligne de commande.

Utilisation de seccomp

Seccomp est une fonctionnalité du Noyau Linux qui permet de filtrer les appels systèmes d'un processus.

Pour cela il faut bien sur que le noyau de votre serveur Linux ait été construit avec cette fonctionnalité activé.

Pour le contrôler :

grep SECCOMP= /boot/config-$(uname -r)

CONFIG_SECCOMP=y
CONFIG_HAVE_ARCH_SECCOMP_FILTER=y
CONFIG_SECCOMP_FILTER=y

Docker utilise par défaut seccomp. Pour le vérifier :

docker run --rm -it test sh
grep Seccomp /proc/$$/status

Seccomp:        2

Récupérons le profil utilisé par défaut par Docker :

wget https://raw.githubusercontent.com/docker/labs/master/security/seccomp/seccomp-profiles/default.json -O custom-profile.json

Pour limiter les droits il suffit d'éditer ce fichier et par exemple si on souhaite désactiver le chmod :


   "defaultAction":"SCMP_ACT_ALLOW",
   "syscalls":[
      {
         "name":"chmod",
         "action":"SCMP_ACT_ERRNO"
      }
   ]

Maintenant lançons le container en utilisant ce profil :

docker run --rm -it --security-opt seccomp:custom-profile.json test sh

chmod 777 /etc/passwd
chmod: /etc/passwd: Operation not permitted

Plus d'infos

Idem pas assez.

Utilisation des User Namespace

Jusqu'à maintenant nous n'avons pas de solution pour restreindre les droits à u user non root appartenant au groupe docker.

Les user namespaces permettent de mapper un user du container vers un utilisateur différent sur la machine hôte. Concrètement, à l'intérieur le container pense être root, mais en fait sur la machine hôte l'utilisateur n'est en fait qu'un utilisateur lambda aux droits limités.

Pour activer cette fonctionnalité de Docker il faut éditer le fichier /etc/docker/daemon.conf et ajouter cette ligne:

{
  "userns-remap" : "default"
}

Ensuite il faut créer un user docker et relancer le service docker :

sudo useradd -d /home/docker -m -g docker -s /bin/bash docker
systemctl daemon-reload && systemctl restart docker

On peut controler mais normalement il a créé user dockremap avec un uid :

cat /etc/subuid |grep dockremap
dockremap:231072:65536

Relancons notre container avec le user root :

[vagrant@builder tesst]$ docker run -v /:/hostdir -it --rm -u root test:latest sh
rm /hostdir/etc/passwd
rm: can't remove '/hostdir/etc/passwd': Permission denied
ls -al /hostdir/etc/passwd

drwxr-xr-x    2 nobody   nobody          57 Aug 31 23:33 yum
lrwxrwxrwx    1 nobody   nobody          12 May 19 09:24 yum.conf -> dnf/dnf.conf
drwxr-xr-x    2 nobody   nobody          98 Oct 23 18:08 yum.repos.d

Les fichiers appartiennent plus à root mais * nobody. Mais pourquoi j'ai pas les droits de les modifier alors ? Je suis root non ?

Le daemon est bien lancé par root ?

ps aux | grep dockerd
root       11572  0.0 12.5 1440772 88672 ?       Ssl  14:42   0:00 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Oui? Bein alors ?

sudo docker info

 Docker Root Dir: /var/lib/docker/231072.231072
 Debug Mode: false
 Registry: https://index.docker.io/v1/
 Labels:
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false

La ligne Docker Root Dir indique que le daemon tourne dans user namespace et qui n'est pas root mais le user avec l'id 231072 donc le user dockremap.

On va alors demander une élévation de privilege

sudo docker run --rm --privileged alpine id
docker: Error response from daemon: privileged mode is incompatible with user namespaces.  You must run the container in the host namespace when running privileged mode.

Docker possède une option permettant de changer de usernamespace.

docker run --rm --privileged --userns=host -u root -v /:/hostdir -it test sh
ls -al /hostdir/etc

drwxr-xr-x    2 root     root            57 Aug 31 23:33 yum
lrwxrwxrwx    1 root     root            12 May 19 09:24 yum.conf -> dnf/dnf.conf
drwxr-xr-x    2 root     root            98 Oct 23 18:08 yum.repos.d

Plus d'infos

Mais comment alors sécuriser docker pour qu'un user lambda n'obtienne pas les droits root ?

Installation de Docker en mode rootless

Le mode rootless permet d'exécuter le démon Docker et les conteneurs en tant qu'utilisateur lambda et sans aucun droit root et même pour l'installation.

En fait le mode rootless exécute le démon Docker et les conteneurs dans un espace de noms utilisateur.

Le test dans une nouvelle VM.

Installation des prérequis :

sudo dnf install -y fuse-overlayfs

Lancement de l'installation sur un compte sans droit root :

curl -fsSL https://get.docker.com/rootless | sh

[INFO] Make sure the following environment variables are set (or add them to ~/.bashrc):

export PATH=/home/vagrant/bin:$PATH
export DOCKER_HOST=unix:///run/user/1000/docker.sock

Ajoutons les lignes à notre .zshrc ou .bashrc:

echo 'export PATH=/home/vagrant/bin:$PATH' >> ~/.bashrc
echo 'export DOCKER_HOST=unix:///run/user/1000/docker.sock' >> ~/.bashrc
source ~/.bashrc

Deux solutions pour le lancer avec ou sans systemd que je vais prendre :

systemctl --user enable docker
systemctl --user start docker
sudo loginctl enable-linger $(whoami)

On se lance en reprenant le meme dockerfile que plus haut :

mkdir test
vi Dockerfile
FROM alpine:3.14.2
RUN mkdir /app && \
    addgroup mongroup && \
    adduser -D -H -s /bin/false -G mongroup monuser && \
    apk --no-cache add libcap
WORKDIR /app
COPY . /app
RUN chown -R monuser:mongroup /app
USER monuser
CMD getpcaps 1
docker build . -t test
docker run --rm --privileged -u root -v /:/hostdir -it test sh
id
1: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap=i
ls -al /hostdir/etc/y*
lrwxrwxrwx    1 root     root            12 Oct 25 13:30 yum.conf -> dnf/dnf.conf
lrwxrwxrwx    1 root     root            24 Oct 25 13:30 yum.repos.d -> .ro061550491/yum.repos.d

Ah les fichiers pointent dans un répertoire .ro061550491 ? Tentons la suppression.

rm /hostdir/etc/passwd

Merde c'est passé. Ma VM est KO ? On sort et on vérifie

ls /etc/passwd
/etc/passwd

En fait, il a été viré du user namespace. Ouf.

Il y a quelques limites à l'utilisation du mode rootless dont voici la liste. La solution n'est pas parfaite, mais pourquoi pas regarder ce que propose RedHat avec podman.

En espérant vous avoir éclairé sur le sujet.