Skip to content

Instantly share code, notes, and snippets.

@moukrea
Last active February 2, 2022 16:31
Show Gist options
  • Save moukrea/1ff72bbac0c61c5dfe826366ae850c11 to your computer and use it in GitHub Desktop.
Save moukrea/1ff72bbac0c61c5dfe826366ae850c11 to your computer and use it in GitHub Desktop.

Bonnes pratiques Docker

Ce gist contient une liste (non exhaustive) de bonnes pratiques à appliquer lors de l'utilisation de Docker, que ce soit du côté du fournisseur (le créateur du service) que de l'utilisateur (la personne qui déploie le service).

Bonnes pratiques Docker - Fournisseur

Bonnes pratiques Docker - Utilisateur

La liste qui suit n'est pas exhaustive, mais vous procurera un bon point de départ pour la mise en place des bonnes pratiques les plus "communes" lors du déploiement d'applications containérisées. Si ce guide est plutôt dédié à des déploiements de petites infrastructures en utilisant simplement Docker ou de petites stacks déployées via docker-compose, la plupart de ces recommandations restent effectives dans de gros environnement de production avec des besoins de scalabilité et résilience importants au travers de Kubernetes et consorts.

Lorsque des commandes sont nécessaires à la mise en place d'une bonne pratique, un exemple générique est fourni pour un système basé sur Debian (Debian, Ubuntu, Kubuntu, PopOS, KDE Neon...)

⚠️ Attention : Les commandes données ici ne sont pas nécessairement adaptées à tous les cas d'usage, elles ne sont fourni qu'à titre d'exemple, utilisez-les avec précaution.


Table des matières

  1. Gardez Docker (et votre système) à jour
  2. Maintenez vos conteneurs à jour (monimage:latest ne suffit pas)
  3. Utilisez un pare-feu
  4. Faîtes en sorte que Docker respecte votre pare-feu (iptables)
  5. Utilisez un reverse proxy sécurisé
  6. Utilisez un outil pour bloquer les adresses IP suspectes (exemple : Fail2ban)
  7. N'ajoutez pas votre utilisateur au groupe docker (sortez-le du groupe si c'est déjà le cas)
  8. Vérifiez que les conteneurs déployés ne soient pas instanciés avec un utilisateur root
  9. Réduisez les privilèges de vos conteneurs autant que possible
  10. Utilisez des images Docker de confiance
  11. Privilégiez les images de la plus petite taille possible
  12. Tirez parti des "Docker secrets"
  13. Utilisez un "Docker Socket Proxy" lors de l'utilisation de conteneurs nécessitant l'accès à la Socket Docker
  14. Limiter/Monitorer l'utilisation en ressource de vos conteneurs
  15. Utilisez des services d'authentification multifacteurs
  16. Utilisez à votre avantage les couches de sécurité offertes par votre DNS
  17. Utilisez des outils de test de sécurité (Exemple : Docker Bench Security, Clair)
  18. Extra : Déployez les conteneurs sur un serveur Linux (évitez les serveurs Windows/MacOS)

✅ Gardez Docker (et votre système) à jour

▲ Top

Les paquets de votre système hôte sont mis à jour régulièrement, corrigeant les éventuelles failles découvertes. Garder son système à jour est une des bonnes pratiques les plus simples à mettre en place.

Commandes : Mettre à jour Docker Pour mettre à jour Docker sur votre système, il suffit de rentrer la (les) commande(s) suivante(s) :
$ apt-get update
$ apt-get --only-upgrade install docker-ce docker-ce-cli containerd.io

Note : Les lignes préfixées par $ ont besoin d'êtres exécutés par un utilisateur privilégié ou préfixées par sudo

Commandes : Mettre à jour les paquets de votre système hôte

Mise à jour de l'ensemble des paquets de votre système

⚠️ Attention : La mise à jour de l'ensemble de vos paquets peut introduire des "breaking changes" introduisant des bugs. Vous pouvez accessoirement faire en sorte que seules les mises à jour de sécurité soient installées, comme décrit dans "Activer les mises à jour de sécurité automatiques" ci-dessous.

Pour mettre à jour l'ensemble des paquets de votre système, entrez les commandes suivantes :

$ apt-get update
$ apt-get upgrade

Activer les mises à jour de sécurité automatiques

À défaut de mettre à jour absolument tous les paquets de votre système au risque d'introduire des bugs en production, il est recommandé d'au moins installer les mises à jour de sécurité le plus régulièrement possible.

Pour ce faire, il suffit de rentrer les commandes suivantes :

$ apt-get install unattended-upgrade
$ dpkg-reconfigure -plow unattended-upgrades
# Suivez les instructions

Note : Les lignes préfixées par $ ont besoin d'êtres exécutés par un utilisateur privilégié ou préfixées par sudo


✅ Maintenez vos conteneurs à jour (monimage:latest ne suffit pas)

▲ Top

⚠️ Attention : Même si avoir tous les conteneurs dans leur toute dernière version est recommandé d'un point de vue sécurité, il est possible que cela donne lieu à des problèmes de compatibilité, surtout si vos conteneurs ont des interdépendances et/ou interagissent avec des bases de données (auquel cas la base de données doit être compatible avec vos conteneurs)

Exemple : Incompatibilité entre deux conteneurs dans leur dernière version La version actuelle de NextCloud (21.0.2) n'est pas compatible avec MariaDB 10.6.3 qui est la dernière version de MariaDB à ce jour.

Si vous instanciez les deux services en utilisant le tag latest, vous ne pourrez pas utiliser NextCloud.

Voici un exemple qui ne fonctionne pas (les commandes sont simplifiées) :

$ docker run -d --name nextcloud nextcloud:latest
$ docker run -d --name mariadb mariadb:latest

Alors que cette version alternative fonctionne parfaitement :

$ docker run -d --name nextcloud nextcloud:latest
$ docker run -d --name mariadb mariadb:10.5

Dans ce cas, la dernière version de NextCloud fonctionne parfaitement avec MariaDB 10.5, mais qui sait... Mais bien que cela soit vrai aujourd'hui, ce n'est pas nécessairement vrai à l'avenir.


En production, il est recommandé de spécifier le plus possible la version de l'image que nous voulons déployer (via ses tags, ou même plus spécifiquement son hash directement) :

Développer l'exemple Via tag :
$ docker run -d -name nginx nginx:1.21.3-alpine
# nginx:1.21.3-alpine plutôt que nginx:alpine

Via hash :

$ docker run -d --name nginx nginx@sha256:1ff1364a1c4332341fc0a854820f1d50e90e11bb0b93eb53b47dc5e10c680116

Note : L'avantage d'utiliser le hash est que si vous déployez le service sur une autre machine, vous serez à 100% d'exécuter la même image. En utilisant les tags, si l'image derrière le tag est mise à jour, la version déployée sur une autre machine peut différer.


C'est ensuite à vous de peser l'impact d'une mise à jour éventuelle avant de procéder et d'éviter au maximum les soucis d'incompatibilité tout en essayant de rester à jour.

Cas pratique Admettons que nous avons un conteneur redis que nous avons mis en place il y six mois avec la commande suivante :
$ docker run -d --name redis redis:latest

Note : Que vous mettiez redis ou redis:latest est la même chose (latest est le tag par défaut).

Dans ce contexte, vous avez récupéré la dernière version de l'image redis il y a six mois, sachant que depuis, de nombreuses mises à jour ont eues lieux.

Mais dans votre cas, vous avez déjà une image redis:latest sur votre machine, vous pouvez exécuter autant de fois que vous voulez la commande précédente, Docker gardera la version que vous avez récupéré il y a six mois.

Pour pouvoir mettre à jour ce conteneur, il faut d'abord l'arrêter, le supprimer, supprimer son image et le relancer :

$ docker stop redis
$ docker rm redis
$ docker rmi redis

Puis relancez votre conteneur comme vous l'aviez initialement lancé il y a six mois :

docker pull redis:latest
docker start -d --name redis redis:latest
# Ou plus simplement
docker run -d --name redis redis:latest

Votre conteneur sera ensuite à jour.


Il existe bien entendu des services dédiés à la gestion des mises à jour des conteneurs que vous exécutez, le plus commun d'entre eux étant Watchtower.

Il propose de grandes options de personnalisation, vous pouvez soit lui laisser tout gérer (en s'exposant à des risques d'incompatibilité comme dit plus tôt), soit lui laisser gérer que les conteneurs non sensibles tout en ignorant les autres et/ou même simplement vous notifier qu'une mise à jour est disponible pour certains de vos conteneurs. Expliquer le déploiement et la configuration de Watchtower dépasse le scope de ce guide


✅ Utilisez un pare-feu

▲ Top

Réduisez la surface d'attaque de votre système hôte en mettant en place un pare-feu afin de bloquer l'accès à des ports qui ne vous sont pas utiles et ouvrir seulement ceux qui vous sont utile.

⚠️ Attention : Docker modifie par lui-même les iptables par défaut, ce qui rend l'utilisation d'un pare-feu en local inutile, suivez la prochaine bonne pratique pour palier au problème.

Commandes : Mise en place d'un firewall simple (UFW) Nous allons mettre en place un firewall simple et ouvrir les ports 80 (HTTP) et 443 (HTTPS) uniquement, en utilisant UFW (Un-complicated FireWall).

⚠️ Attention : Lisez attentivement les instructions, au risque de ne plus pouvoir accéder au serveur.

Note : Les lignes préfixées par $ ont besoin d'êtres exécutés par un utilisateur privilégié ou préfixées par sudo

UFW est généralement installé par défaut, vérifiez que ce soit bien le cas avec la commande suivante :

which ufw

Si la commande ne retourne rien, ufw n'est pas installé. Pour l'installer :

$ apt-get update
$ apt-get install ufw

Vérifiez le status d'ufw, par défaut il devrait être désactivé :

ufw status

Si vous obtenez Status: inactive, tout est bon.
⚠️ Attention : Si vous êtes connecté à la machine via SSH et que vous obtenez Status: active, il est recommandé de désactiver temporairement ufw pour ne pas vous bloquer (possiblement définitivement) pendant la configuration :

$ ufw disable

Nous allons ensuite bloquer l'ensemble du trafic entrant sur la machine :

$ ufw default deny incoming

Par simplicité, nous allons autoriser le trafic sortant :

$ ufw default allow outgoing

Puis autoriser les ports 80 (HTTP) et 443 (HTTPS) en trafic entrant :

$ ufw allow 80
$ ufw allow 443

Note : Si vous utilisez Nginx comme reverse proxy et qu'il est installé sur la même machine que votre firewall, vous pouvez remplacer ces deux règles par $ ufw allow in "Nginx Full" qui sera plus spécifique en ouvrant les ports 80 et 443 à Nginx uniquement

⚠️ Attention : Si vous êtes connecté à cette machine via SSH, ouvrez le port utilisé par SSH pour pouvoir continuer à vous connecter une fois le pare-feu activé :

$ ufw allow ssh

Note : Cette commande ouvrira l'accès au port 22 en TCP (comme $ ufw allow 22/tcp), si vous utilisez un autre port pour SSH, veuillez le spécifier explicitement, par exemple $ ufw allow 2222/tcp

Note 2 : Si vous utilisez OpenSSH pour votre connexion SSH, vous pouvez remplacer cette règle par $ ufw allow OpenSSH qui sera plus spécifique en ouvrant le port SSH à OpenSSH uniquement.

La configuration d'ufw est alors terminée, vous pouvez vérifier cette dernière via la commande suivante :

ufw status verbose

Pour terminer, il ne vous reste plus qu'à activer ufw :

$ ufw enable

Cette configuration est relativement simpliste, mais fourni une couche de protection minimale, ufw permet des configurations bien plus avancées et/ou spécifiques, mais cela dépasse largement le scope de ce guide.


✅ Faîtes en sorte que Docker respecte votre pare-feu (iptables)

▲ Top

Il s'agit d'un problème connu de Docker (voir l'issue ouverte sur Github). Docker ne respecte pas les règles de votre pare-feu système par défaut. Il est possible de remédier à ce problème en éditant la configuration de Docker.

Commandes : Désactiver la modification des iptables par Docker Note : *Les lignes préfixées par `$` ont besoin d'êtres exécutés par un utilisateur privilégié ou préfixées par `sudo`*

Pour faire en sorte que Docker ne modifie pas les iptables de votre système, veuillez éditer le fichier /etc/default/docker :

$ nano /etc/default/docker
# Utilisez vi si votre système ne possède pas nano

Ajoutez-y la ligne suivante et sauvegardez les modifications :

DOCKER_OPTS="--iptables=false"

Pour que les modifications soient prises en compte, veuillez relancer le service Docker :

$ service docker restart
# OU
$ systemctl restart docker

✅ Utilisez un reverse proxy sécurisé

▲ Top

Plutôt que d'exposer votre service directement, à l'extérieur (que ce soit localement ou sur le web) il est recommandé d'exposer vos services au travers d'un reverse proxy, qui sera chargé de vous rediriger au bon endroit.

Vulgarisation ⚠️ **Attention :** Il ne s'agit là que d'une vulgarisation des possibilités d'un reverse proxy, détailler l'ensemble de ce qu'offre un reverse proxy dépasse le scope de ce guide. **Nous ne traiterons pas la mise en place d'un reverse proxy dans ce guide, car cela serait trop verbeux, même pour une installation simple.**

Admettons que vous avez trois services que vous voulez exposer à l'extérieur de votre système hôte, plutôt qu'ouvrir les ports (par exemple) 80, 8080 et 8888 tout en laissant l'utilisateur spécifier le port via la barre d'adresse de son navigateur (ce qui n'est pas spécialement intuitif), vous pouvez exposer seulement le port 80 vers votre reverse proxy, qui s'occupera de la redirection vers le bon service. Cela permet (en plus de simplifier l'utilisation) de réduire considérablement la surface d'attaque de votre système.

Schéma (avant la mise en place d'un reverse proxy) :

Client                                Serveur
╔════════════════════════════════╗         ╔════════════════════════════════════╗
║                                ║         ║                 ┌────────────────┐ ║
║ http://exemple.com ───────────═╬════/════╬═────────────────┤ container:80   │ ║
║                                ║         ║                 └────────────────┘ ║
║                                ║         ║                 ┌────────────────┐ ║
║ http://exemple.com:8080 ──────═╬════/════╬═────────────────┤ container:8080 │ ║
║                                ║         ║                 └────────────────┘ ║
║                                ║         ║                 ┌────────────────┐ ║
║ http://exemple.com:8888 ──────═╬════/════╬═────────────────┤ container:8888 │ ║
║                                ║         ║                 └────────────────┘ ║
╚════════════════════════════════╝         ╚════════════════════════════════════╝

Schéma (après la mise en place d'un reverse proxy) :

Client                                     Serveur
╔════════════════════════════════╗         ╔════════════════════════════════════╗
║                                ║         ║                 ┌────────────────┐ ║
║ http://service.exemple.com ─┐  ║         ║               ┌─┤ container:81   │ ║
║                             │  ║         ║  ┌──────────┐ │ └────────────────┘ ║
║                             │  ║         ║  │          ┼─┘ ┌────────────────┐ ║
║ http://service2.exemple.com ┼─═╬════/════╬═─┼ proxy:80 ┼───┤ container:8080 │ ║
║                             │  ║         ║  │          ┼─┐ └────────────────┘ ║
║                             │  ║         ║  └──────────┘ │ ┌────────────────┐ ║
║ http://service3.exemple.com ┘  ║         ║               └─┤ container:8888 │ ║
║                                ║         ║                 └────────────────┘ ║
╚════════════════════════════════╝         ╚════════════════════════════════════╝

Votre reverse proxy peut aussi aisément forcer la redirection HTTP vers HTTPS ainsi qu'exposer des services fonctionnant exclusivement en HTTP derrière du HTTP, améliorant considérablement la sécurité.

En plus de cela, la plupart des reverse proxy offrent aussi des possibilités de load balancing, nous n'en parleront pas ici, car cela dépasse le scope de ce guide.


Il existe des dizaines de services vous permettant de mettre en place un reverse proxy et/ou load balancer, pour en citer quelques-uns :

Note : Nginx est probablement le plus simple d'entre eux, Traefik est fortement recommandé à la fois par Docker et la communauté... Caddy est relativement simple à mettre en place, HAProxy est plutôt destiné à une utilisation professionnelle, tandis que Nginx Proxy Manager est plutôt destiné à une utilisation personnelle, avec l'avantage d'être très facilement mis en place.


✅ Utilisez un outil pour bloquer les adresses IP suspectes (exemple : Fail2ban)

▲ Top

Il existe des outils permettant de bloquer l'accès à des adresses IP présentant de mauvaises intensions et/ou suspectes automatiquement. Généralement, ils s'appuient sur les fichiers de logs et cherchent à repérer les tentatives d'exploitation de failles, les mots de passes erronés à répétition (tentatives de brute force) et bloquent les adresses IP suspectes en conséquence.

L'un des outils les plus simples pour remplir ce rôle est Fail2ban. Certains pare-feux, reverse proxies (ex : Traefik), load balancers et/ou même certains services d'authentification (ex : Authelia) gèrent aussi cela.

Commandes : Mise en place de Fail2ban Note : *Les lignes préfixées par `$` ont besoin d'êtres exécutés par un utilisateur privilégié ou préfixées par `sudo`*

⚠️ Attention : Nous ne couvrirons que l'installation de Fail2ban par défaut, veuillez vous référer à la documentation officielle, Veuillez considérer le risque de vous bloquer hors de la machine si vous êtes connecté via SSH.

Vérifiez si fail2ban est présent sur votre système (ça ne devrait pas être le cas par défaut) :

which fail2ban

Si la commande précédente ne retourne rien, installez fail2ban :

$ apt-get update
$ apt-get install fail2ban

Copiez ensuite les fichiers de configuration de fail2ban afin qu'ils ne soient pas écrasés lors de prochaines mises à jour du service :

$ cp /etc/fail2ban/jail.{conf,local}
$ cp /etc/fail2ban/fail2ban.{conf,local}

Note : Les fichiers *.conf sont interprétés en premier, les règles contenues dans les fichiers *.local écrasent ensuite celles contenues dans les fichiers *.conf.

Configurez fail2ban à convenance (cela dépasse le scope du guide) en vous référant à la documentation officielle, nous utiliserons simplement la configuration par défaut ici.

Le service devrait être activé automatiquement, pour vérifier, faîtes :

$ service ufw status
# OU
$ systemctl status ufw

Si le service n'est pas démarré, démarrez le via les commandes suivantes :

$ service fail2ban start
# OU
$ systemctl start fail2ban
$ systemctl enable fail2ban # Démarre le service automatiquement après redémarrage

Si le service était démarré, mais que vous avez modifié sa configuration, veuillez le redémarrer :

$ service fail2ban restart
# OU
$ systemctl restart fail2ban

Fail2ban est maintenant en place.


✅ N'ajoutez pas votre utilisateur au groupe docker (sortez-le du groupe si c'est déjà le cas)

▲ Top

Il est fortement probable que lors de l'installation de Docker sur votre machine, votre utilisateur ait été automatiquement ajouté au groupe docker.

Bien que très pratique, car vous n'avez pas besoin d'exécuter les commandes en tant qu'utilisateur privilégié (en étant connecté à votre machine en tant que root ou en utilisant la commande sudo), cela peut poser de gros problèmes de sécurité tels que l'escalade de privilèges, pouvant permettre à un conteneur compromis d'accéder à la machine hôte.

Commandes : Vérifier que votre utilisateur ne fait pas partie du groupe `docker` (et en sortir le cas échéant) Note : *Les lignes préfixées par `$` ont besoin d'êtres exécutés par un utilisateur privilégié ou préfixées par `sudo`*

Listez l'ensemble des groupes auxquels appartient votre utilisateur :

groups ${USER}

Si le retour de cette commande contient docker, votre utilisateur fait partie du groupe docker. Pour le retirer, entrez la commande suivante :

$ gpasswd -d ${USER} docker

Dorénavant, vous aurez systématiquement besoin d'exécuter les commandes Docker par le biais d'un utilisateur privilégié (en tant que root ou en préfixant vos commandes par sudo).


✅ Vérifiez que les conteneurs déployés ne soient pas instanciés avec un utilisateur root

▲ Top

Beaucoup d'images (y compris les images officielles) s'instancient au travers d'un utilisateur root (privilégié). Cela peut poser de gros problèmes de sécurité tels que l'escalade de privilèges, pouvant permettre à un conteneur compromis d'accéder à la machine hôte.

C'est par exemple le cas des images Apache (httpd), apache nécessitant un utilisateur privilégié pour être exécuté correctement.

Commandes : Vérifier que votre container n'utilise pas l'utilisateur `root` Note : *Si vous avez appliqué le point ["N'ajoutez pas votre utilisateur au groupe `docker`"](#najoutez-pas-votre-utilisateur-au-groupe-docker-sortez-le-du-groupe-si-cest-déjà-le-cas), vous devrez préfixer vos commandes Docker par `sudo`.*

Nous prendrons l'exemple d'un conteneur apache. Exécutons dans un premier temps le conteneur :

$ docker run -d --name apache httpd:alpine

Entrons ensuite dans le terminal de ce conteneur :

$ docker exec -it apache sh

Une fois dans le conteneur, entrez la commande suivante pour voir quel utilisateur est utilisé par ce dernier :

whoami

Dans le cas d'apache, l'utilisateur retourné devrait être root, ce qui pose un problème.

Dans certains cas, il est possible de palier au problème en ajoutant l'argument --user 1000:1000 (ajustez l'ID à convenance) à la commande d'exécution, comme suit :

$ docker run -d --name apache --user 1000:1000 httpd:alpine

Malheureusement dans le cas d'apache, cela empêche d'exécuter le conteneur correctement :

[core:error] [pid 1:tid 140120762944328] (13)Permission denied: AH00099: could not create /usr/local/apache2/logs/httpd.pid.XXXXXX

En effet, apache a besoin de privilèges pour être exécuté correctement. Il est quand même possible de palier au problème de plusieurs façons, en cherchant une autre image qui ne s'instancie pas en tant que root ou en modifiant l'image soit-même.

Modifier une image exécutée en tant que `root` pour qu'elle ne soit pas exécutée avec des privilèges élevés ⚠️ **Attention :** Il est possible de procéder de multiples façons, ces instructions ne représentent pas une solution à toutes épreuves.

Comme précédemment, nous utiliserons l'image apache officielle à titre d'exemple. Étant donné qu'il est impossible d'exécuter l'image avec un utilisateur spécifique directement, nous allons modifier l'image par nous-mêmes.

Pour ce faire, créons un fichier Dockerfile avec les instructions suivantes :

# Basons nous sur l'image apache officielle
FROM httpd:alpine

# Donnons les droits d'accès à apache à l'utilisateur www-data
RUN chown -hR www-data:www-data /usr/local/apache2/

# Utiliser setcap pour avoir la possibilité de lier des ports privilégiés en tant qu'utilisateur non-root
RUN apk update && apk add --no-cache libcap 
RUN setcap 'cap_net_bind_service=+ep' /usr/local/apache2/bin/httpd
RUN getcap /usr/local/apache2/bin/httpd

# Exécutons le container avec l'utilisteur www-data
USER www-data

Nous allons ensuite construire l'image depuis l'endroit où nous avons créé le Dockerfile avec la commande suivante :

$ docker image build . -t myname/myimage:apache-non-root

Note : Le "." représente l'emplacement du fichier Dockerfile, ce qui est situé après le "-t" représente le nom (et le tag, après les deux points) de l'image.

Exécutez le conteneur via la commande suivante :

$ docker run -d --name apache myname/myimage:apache-non-root

Vérifiez ensuite quel est l'utilisateur utilisé par le conteneur (en suivant les instructions ci-dessus), vous verrez qu'il s'agit en effet de www-data.


Certaines images proposent des solutions pour ne pas exécuter les process du conteneur en tant que root.

C'est par exemple le cas de la plupart (voire toutes) les images fournies par linuxserver.io. Elles proposent de personnaliser l'utilisateur de la façon suivante :

Cliquez pour développer L'exemple est tiré de l'image Heimdall, qui se trouve être l'image officielle du service (mais hébergée sur [linuxserver.io](https://www.linuxserver.io/))

Via l'invite de commandes :

docker run -d \
  --name=heimdall \
  -e PUID=1000 \
  -e PGID=1000 \
  -e TZ=Europe/London \
  ghcr.io/linuxserver/heimdall

Via Docker Compose :

---
version: '3.9'

services:
  heimdall:
    image: ghcr.io/linuxserver/heimdall
    container_name: heimdall
    environment:
      - PUID=1000
      - PGID=1000
    ...

Dans les deux cas, spécifiez votre utilisateur par la valeur donnée à PUID (ID de l'utilisateur), PGID (ID du groupe).


✅ Réduisez les privilèges de vos conteneurs autant que possible

▲ Top

Il est parfois difficile de contourner le précédent point (un conteneur exécuté en tant que root), et même en le contournant, un conteneur compromis peut potentiellement permettre une escalade de privilèges, donnant un accès root à la machine hôte.

Un conteneur n'est qu'une abstraction de la couche applicative d'un système d'exploitation, le kernel reste celui de la machine hôte.

Il est donc important de veiller à ce qu'un conteneur ne puisse pas obtenir plus de privilèges qu'il n'en a par défaut.

⚠️ Important : Il est possible dans certains cas que vous soyez obligé d'exécuter un ou plusieurs conteneurs de façon privilégié malgré tout, dans ce cas, vous pouvez essayer de faire en sorte que l'id (utilisateur et groupe) de l'utilisateur du conteneur corresponde à un utilisateur restreint sur votre machine hôte (voir point précédent : "Vérifiez que les conteneurs déployés ne sont pas instanciés avec un utilisateur root").

Commandes : Un moyen simple d'empêcher l'escalade de privilèges Note : *Si vous avez appliqué le point ["N'ajoutez pas votre utilisateur au groupe `docker`"](#✅-najoutez-pas-votre-utilisateur-au-groupe-docker-sortez-le-du-groupe-si-cest-déjà-le-cas), vous devrez préfixer vos commandes Docker par `sudo`.*

Une des plus simples façons de précéder est de stipuler à Docker que nous ne voulons pas que le conteneur obtienne de nouveaux privilèges durant son fonctionnement. Par exemple via la commande docker run :

$ docker run -d --security-opt=no-new-privileges:true httpd

Note : L'argument qui nous intéresse ici est --security-opt=no-new-privileges:true

Ou via Docker Compose :

---
version: '3.9'

services:
  apache:
    image: httpd
    security_opt:
      - no-new-privileges:true
    ...

Évitez autant que possible d'exécuter vos conteneurs en mode privilégié comme dans l'exemple ci-dessous :

Développer l'exemple d'exécution de conteneurs privilégiés Note : *Si vous avez appliqué le point ["N'ajoutez pas votre utilisateur au groupe `docker`"](#✅-najoutez-pas-votre-utilisateur-au-groupe-docker-sortez-le-du-groupe-si-cest-déjà-le-cas), vous devrez préfixer vos commandes Docker par `sudo`.*
$ docker run --privileged httpd

Ou via Docker Compose :

---
version: '3.9'

services:
  apache:
    image: httpd
    privileged: true
    ...

✅ Utilisez des images Docker de confiance

▲ Top

Utilisez tant que possible les images officielles, ou les mieux notées et téléchargés ayants des mises à jour les plus récentes possibles.

Les images fournies par linuxserver.io sont aussi généralement plutôt sûres et offrent (toutes ?) la possibilité de personnaliser l'utilisateur exécutant le processus principal du conteneur (voir point "Vérifiez que les conteneurs déployés ne sont pas instanciés avec un utilisateur root").

Si l'image Docker que vous souhaitez utiliser provient d'un "Docker registry" privé, veuillez vous assurer que l'éditeur de cette image est digne de confiance.


✅ Privilégiez les images de la plus petite taille possible

▲ Top

Plus une image est grande, plus le nombre de paquets préinstallés est grande, plus la surface d'attaque est grande. Même au niveau du système d'exploitation, plus ce dernier est léger, moins grande est la surface d'attaque.

Il est donc préférable de privilégier des images basées sur Alpine que Debian.

Pour illustrer cela, voici un report des vulnérabilités trouvées dans les images "OS" les plus répandues :

Image Vulnérabilités
Debian 55
Debian (slim) 42
Ubuntu 31
Centos 1
Fedora 0
Alpine 0

Source : Shifting Docker security left (Snyk)


✅ Tirez parti des "Docker secrets"

▲ Top

Docker dispose d'une fonctionnalité permettant de gérer les "secret", autrement dit des données sensibles (telles que des mots de passes et/ou données personnelles) que vous ne voulez pas nécessairement saisir en dur dans votre invite de commande (étant visibles dans l'historique) ou à la portée de tous dans votre fichier docker-compose.yaml

Prenons l'exemple de l'exécution d'un conteneur MariaDB :

$ docker run -d --name mariadb -e MYSQL_ROOT_PASSWORD=My1n\$€cùr3p@\$§w0Rd -p 3306:3306 mariadb

Après avoir exécuté cette commande, on peut retrouver le mot de passe en clair dans l'historique du terminal.

Mini guide : Utiliser les "secrets" Docker Commencez par créer un dossier dans lequel vous allez stocker vos secrets Docker :
mkdir ~/secrets

Note : L'emplacement importe peu, la "home directory" de l'utilisateur est utilisée ici à titre d'exemple.

Apposez les restrictions de permission sur ce dossier comme bon vous semble (le plus limité, le mieux), puis créez votre premier secret :

echo 'My1n$€cùr3p@$§w0Rd' >> ~/secrets/mysql_root_password
$ docker secret create mysql_root_password ~/secrets/mysql_root_password

Exécutez votre service en tirant parti de votre nouveau secret :

docker run -d --name mariadb -e MYSQL_ROOT_PASSWORD_FILE="/run/secrets/mysql-root-password" mariadb

Note : Remarquez l'ajout de "_FILE" à la variable MYSQL_ROOT_PASSWORD, sans cela, le secret ne secret ne serait pas lu et la valeur de MYSQL_ROOT_PASSWORD serait "/run/secrets/mysql_root_password"

Pour Docker Compose, le fonctionnement est différent, il ne sera pas traité dans ce guide, mais vous pouvez mettre en place la même pratique.


✅ Utilisez un "Docker Socket Proxy" lors de l'utilisation de conteneurs nécessitant l'accès à la Socket Docker

▲ Top

Certains services comme Traefik et Portainer nécessitent l'accès à la socket Docker. Si le conteneur ayant accès à la socket de Docker est compromis, l'attaquant peut obtenir accès à la machine hôte (voir la documentation officielle de Docker à ce propos).

Il est dans ce cas fortement préférable d'utiliser un "Docker Socket Proxy". Il existe des dizaines de services qui peuvent remplir ce rôle, mais la documentation de Traefik recommande l'utilisation de Docker Socket Proxy de Tecnativa.

Note : Expliquer comment déployer ce genre de service est hors du scope de ce guide.


✅ Limiter/Monitorer l'utilisation en ressource de vos conteneurs

▲ Top

Par défaut Docker utilise autant de ressources qu'il lui est possible d'utiliser sur le système. Si l'ensemble des ressources sont utilisées par plusieurs conteneurs cela peut causer des problèmes de crashes ou causer des comportements inattendus, qui, de fait, peuvent introduire des problèmes de sécurité.

Pour permettre une plus grande stabilité, essayez de monitorer l'utilisation en ressource de vos conteneurs pour déduire quel service à besoin de plus de ressources qu'un autre ou encore déterminer s'il est préférable d'augmenter les capacités de votre serveur.

La mise en place de cette pratique étant propre à chacun, nous vous invitons à consulter la documentation officielle de Docker à ce propos.


✅ Utilisez des services d'authentification multifacteurs

▲ Top

Un service bénéficiant d'une authentification à multiple facteur bénéficie d'une sécurité plus élevée, mais si ce dernier est compromis ou faillible, vous vous exposez à des risques. Il existe des services dont le seul but est d'assurer une authentification (LDAP, 2FA, TOTP, Duo et même Yubikey) sécurisée, qui peuvent s'utiliser en intermédiaire entre votre reverse proxy et le service sécurisé en lui même.

C'est le cas d'Authelia qui est une référence en la matière, s'adaptant parfaitement au reverse proxy Traefik


✅ Utilisez à votre avantage les couches de sécurité offertes par votre DNS

▲ Top

Certains fournisseurs DNS proposent des couches de sécurité supplémentaires tels que le masquage d'IP de votre serveur (une IP du DNS est retourné à la place), des protections anti DDoS, détection de comportements frauduleux... Utilisez-les à votre avantage.

Cloudflare est très intéressant en ce sens et permet d'accéder à une couche de sécurité supplémentaire facilement.

Note : Expliquer comment configurer votre nécessaire pour une sécurité renforcée dépasse le scope de ce guide, d'autant qu'il y a au moins autant de façons de faire que de fournisseurs DNS.


✅ Utilisez des outils de test de sécurité (Exemple : Docker Bench Security, Clair)

▲ Top

Pour finir, une fois vos services déployer, il peut être intéressant d'utiliser des outils destinés à tester vos services déployés pour les sources de risque les plus répandues et agir en conséquence. En corrigeant les faille éventuelles remontées par ces outils, vous vous retrouverez avec un système plus sur, et moins exposé à des risques.

Il existe de multiples outils pour cela :

Note : L'utilisation de ces services dépasse le scope de ce guide.


Extra : Déployez les conteneurs sur un serveur Linux (évitez les serveurs Windows/MacOS)

▲ Top

L'implémentation de Docker sur Windows (via WSL) et MacOS est très (trop) permissive pour la production. Les permissions/droits d'accès ne sont pas systématiquement respectées et l'utilisateur docker est privilégié.

Au contraire, Docker sur Linux permet plus de contrôle/granularité et est bien plus adapté à la production. Docker est aussi natif à Linux, et permet donc de meilleures performances.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment