Cet article a pour objectif de présenter les paramètres de sécurité à appliquer aux technologies de conteneurisation Docker. Il décrit les bonnes pratiques pour créer une image Docker depuis le fichier Dockerfile
.
Création d’une image Docker
Une image Docker est construite à l’aide d’un fichier Dockerfile
qui consiste en une suite d’instructions interprétées par le client Docker lors de la création de l’archive de l’image. Certaines instructions constituent une couche du conteneur (layer). L’objectif est donc de s’assurer que chaque couche apporte le maximum de sécurité.
La documentation Docker présente les différents choix et lignes de commande pour construire une image. À titre d’exemple, la command suivante utilise un fichier Dockerfile
et le dossier actuel pour construire une image :
docker build -f Dockerfile .
Indiquer des méta-data sur l’image
L’instruction LABEL
permet d’ajouter des informations de type clé-valeur. Ces informations permettent de tracer qui est le propriétaire de l’image, les outils utilisés ou encore les licences. Il convient donc de compléter d’ajouter au moins un champs indiquant le responsable de l’image. Cette personne (ou entité) sera en charge de mettre à jour l’image tout au long de la vie du projet :
LABEL equipe=""
LABEL responsable=""
LABEL licence=""
LABEL description=""
L’instruction ne va pas modifier la manière dont l’image s’exécute, elle permettra uniquement de faciliter la manière de gérer les images dans le groupe. Les Labels peuvent être ajoutés automatiquement lors de la création de l’image dans une usine logicielle (CI/CD).
Utiliser des images minimales et officielles
L’image de base doit contenir un minimum de données afin de limiter la surface d’exposition lors d’une attaque. Ainsi, il convient d’utiliser tant que possible des images minimales avec uniquement le strict mimimum d’outils installés.
Dans une premier temps, il est recommandé de choisir des images officielles depuis le DockerHub. Pour cela il est possible d’ajouter le tag official
lors d’une recherche :
https://hub.docker.com/search?q=nginx&image_filter=official&type=image
Malheureusement, les images officielles ne sont pas mise à jour quotidiennement et donc peuvent inclure des binaires vulnérables. Il est alors possible de redéployer ses propres images depuis les fichiers Dockerfile
officiels. Pour reprendre notre exemple, l’image officielle du serveur nginx est ici :
https://github.com/nginxinc/docker-nginx/blob/master/mainline/debian/Dockerfile
Utiliser des images avec des versions spécifiques
Il est recommandé de ne pas utiliser le tag latest
lors de la création d’une image. En effet, en cas de présence d’une vulnérabilité (CVE) sur l’image de base il est alors plus difficile d’identifier si l’image de base de vulnérable si elles utilisent le tag latest
au lieu d’une version. Il convient alors d’utiliser une version précise pour chaque image. Par exemple ici la version 18.04 d’Ubuntu est utilisée :
FROM ubuntu:18.04
Dans le cas de la création d’une image, alors faudra pointer vers la dernière image publiée.
Utiliser le multi-stage et les cibles pour chaque environnement
Depuis la version 17.05 de Docker, il est possible de déclarer au sein d’un même fichier Dockerfile
plusieurs étapes de construction de l’image, appelé multi-stage.
Cette technique permet de réduire drastiquement la taille de l’image du conteneur en évitant d’inclure les couches de construction intermédiaires. Il est alors possible de déposer au sein dans une image intermédiaire les outils de construction de l’application, comme par exemple une clef SSH. Cette clef ne sera alors plus présente dans l’image finale.
FROM ubuntu:18.04 as intermediate
WORKDIR /app
# Ici la clef est copiée dans l'image intermediate
COPY secret/key /tmp/
# Elle est utilisée pour se connecter à un serveur distant
RUN scp -i /tmp/key [email protected]/files .
FROM ubuntu:18.04
WORKDIR /app
# Les fichiers sont ensuites copiés depuis l'image intermediate sans la clef SSH
COPY --from=intermediate /app .
Dans ce Dockerfile
, l’image intermediate
contient une clef SSH qui ne sera plus présente dans l’image finale. Cette technique peut être utilisée pour faciliter la construction et déploiement d’images en production sans inclure l’environnement de développement.
Installer le minimum de dépendances
Avec l’utilisation d’une image minimale, il est souvent nécessaire d’ajouter des dépendances avec yum
ou apt
. Il est recommandé d’installer uniquement les paquets nécessaires à l’application. De plus, il n’est pas recommandé de mettre à jour l’image ; celle-ci doit être réalisée via l’utilisation d’une nouvelle version de l’image de base (cf point précédent). Ainsi, la commande RUN apt-get upgrade
et dist-upgrade
ne sont pas recommandées.
En outre, il est recommandé de mettre à jour le cache des dépendances et l’installation dans la même commande :
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
&& rm -rf /var/lib/apt/lists/*
Cela va avoir deux avantages :
- Eviter la création d’une nouvelle couche pour chaque commande
- Forcer la mise à jour du cache et donc installer la dernière version des paquets
Enfin, il est possible de spécifier la version du composant que l’on souhaite installer. Cette technique, appelée version pinning, permet d’installer une version spécifique d’un paquet que l’on sait corriger une vulnérabilité qui pourrait déjà être présente dans l’image de base.
Appliquer le principe de moindre privilèges
Comme pour une application sur un serveur, il convient de créer un utilisateur dédié pour l’exécuter au sein du conteneur. La création d’un utilisateur peut dépendre de chaque OS, l’utilisation d’un utilisateur dédié requiert l’instruction USER
.
Par exemple, pour notre image Ubuntu, le fichier contient le code suivant :
RUN groupadd -r appuser && useradd -r -s /bin/false -g appuser appuser
USER appuser
Après cette ligne les commandes seront exécutées avec l’utilisateur appuser
qui n’est plus root
et qui n’a donc plus les privilèges suivants dans le conteneur :
- Permissions sur l’ensemble du système de fichiers
- Restriction sur l’utilisation des ports inférieurs à 1024
- Absence des permissions nécessaires pour exécuter des commandes de debug
Il faudra penser à exécuter la commande suivante pour donner les droits à l’utilisateur sur le dossier de l’application :
RUN chown -R appuser:appuser /app
Enfin, il est à noter que ce nouvel utilisateur n’est utilisé que pour exécuter le processus dans le conteneur, il n’a donc pas de de mot de passe associé.
Exposer un port
L’instruction EXPOSE
permet d’exposer un port du conteneur sur l’hôte. Cette instruction est équivalente à l’option -p
. Il convient de l’utiliser avec le port par défaut de l’application et supérieur à 1024 (afin de respecter la recommandation liée à l’utilisateur non-root).
Utiliser l’instruction COPY plutôt que ADD
Afin de copier des fichiers de l’hôte vers le conteneur, Docker propose deux instructions :
COPY
: copie récursivement les fichiers de la source à la destination (explicite) et accepte les URLADD
: copie récursivement les fichiers de la source, crée implicitement le dossier de destination et accepte les archives et les URL
L’utilisation de l’instruction ADD
peut introduire les faiblesses suivantes :
- Le téléchargement du fichier pointé par l’URL n’est pas forcément réalisé de manière sécurisé (interception SSL possible)
- Extraction automatique de l’archive ce qui peut rendre possible des attaques de type zip-bomb
Il est donc recommandé d’utiliser uniquement l’instruction COPY
et donc d’interdire l’utilisation de l’instruction ADD
.
L’instruction ADD
peut être utile pour copier un fichier zip et le décompresser directement dans l’archive sans avoir à installer l’outil zip
. Pour cela, il est possible d’utiliser un fichier avec une étape (multi-stage) préliminaire permettant de récupérer le fichier puis ensuite de le décompresser.
RUN curl <chemin_fichier> -O <dossier_destination> \
&& unzip <dossier_destination>/<fichier>.zip -d <dossier_destination> \
&& rm <dossier_destination>/<fichier>.zip
Contrôler les fichiers copiés dans l’image
La commande suivante copie l’ensemble des fichiers du contexte Docker dans l’image :
COPY . .
L’ensemble des fichiers présents dans le dossier, dont les fichiers cachés et potentiellement des secrets, seront copiés dans l’image. C’est par exemple le cas du répertoire .git
qui contient l’ensemble des informations du code source. Il convient alors de créer un fichier .dockerignore
qui liste les fichiers à ignorer. La documentation officielle décrit la syntaxe à utiliser pour ce fichier.
Points de contrôle
Le tableau suivant consolide les points de contrôle :
Point de contrôle | Commande Docker |
---|---|
Utiliser des images minimales et officielles | |
Utiliser des images avec des versions spécifiques | FROM image:version |
Utiliser le multi-stage et les cibles pour chaque environnement | FROM … AS … |
Installer le minimum de dépendances | RUN |
Appliquer le principe de moindre privilèges | USER |
Exposer un port | EXPOSE |
Utiliser l’instruction COPY plutôt que ADD | COPY & ADD |
Contrôler les fichiers copiés dans l’image | COPY |
Vérifier l’image
Le client Docker possède une commande pour tester la sécurité d’une image.
docker scan Dockerfile
Le client va envoyer des informations au service Snyk pour identifier les vulnérabilités :
Docker Scan relies upon access to Snyk, a third party provider, do you consent to proceed using Snyk? (y/N)
Ce service est externe, il est recommandé de s’assurer qu’aucune fuite d’information ne se fait via des données présentes dans le fichier Dockerfile
.