Chapitre 10 : Sécurité des conteneurs¶
Objectifs du chapitre
- Expliquer ce que l'isolation d'un conteneur garantit, et ce qu'elle ne garantit pas
- Exécuter un conteneur avec un utilisateur non privilégié
- Utiliser un système de fichiers en lecture seule et réduire les capabilities d'un conteneur
- Fournir un secret à une application sans l'inscrire dans l'image ni dans une variable d'environnement
- Lire un rapport d'analyse de vulnérabilités et décider d'une correction
- Acquis d'apprentissage visé : AA7
1. Ce que l'isolation garantit, et ce qu'elle ne garantit pas¶
Au chapitre 1, vous avez vu qu'un conteneur est un processus isolé par les namespaces et limité par les cgroups, mais qui partage le noyau de l'hôte. Cette isolation est réelle, mais elle est plus légère que celle d'une machine virtuelle : une faille du noyau, ou une mauvaise configuration du conteneur, peut permettre à un attaquant de dépasser les limites du conteneur.
La sécurité des conteneurs repose donc sur deux idées :
- réduire la surface d'attaque : moins de composants, moins de services exposés (chapitres 6 et 9) ;
- appliquer le principe du moindre privilège : un conteneur ne reçoit que les droits dont il a besoin, rien de plus.
Ce chapitre présente cinq leviers concrets.
flowchart TB
A["Application dans un conteneur"] --> U["1. Utilisateur non root"]
A --> R["2. Système de fichiers en lecture seule"]
A --> C["3. Capabilities réduites"]
A --> S["4. Secrets hors de l'image"]
A --> V["5. Image sans vulnérabilité connue"]
Rappel : l'accès à Docker est un accès administrateur
Quiconque peut lancer des conteneurs sur une machine (membre du groupe docker) peut, avec un bind mount, lire ou modifier n'importe quel fichier de l'hôte. L'accès au moteur Docker équivaut à un accès administrateur à la machine.
2. Utilisateur non root¶
2.1 Le problème¶
Par défaut, le processus d'un conteneur s'exécute en tant que root (identifiant 0). Vous pouvez le vérifier :
Pour le noyau, cet utilisateur 0 est, par défaut, le même que l'utilisateur 0 de l'hôte : seules les limites du conteneur (namespaces, capabilities) l'en séparent. Si l'application est compromise, ou si le conteneur est mal configuré (volume sensible monté, droits supplémentaires), l'attaquant dispose de droits maximaux.
2.2 Exécuter avec un utilisateur ordinaire¶
On crée un utilisateur dans l'image, puis on l'active avec l'instruction USER. Les commandes suivantes, y compris CMD, s'exécutent avec cet utilisateur.
| Élément | Rôle |
|---|---|
useradd ou adduser |
Crée l'utilisateur sans mot de passe ; sur Debian, --system marque un compte de service |
--uid 10001 |
Fixe un identifiant numérique explicite, utile pour les droits sur des volumes |
COPY --chown=... |
Donne la propriété des fichiers copiés à l'utilisateur |
USER appuser |
Les instructions et le conteneur s'exécutent désormais avec cet utilisateur |
Placez USER après les instructions qui exigent des droits d'administrateur (installation de paquets, par exemple).
On peut aussi imposer l'utilisateur au lancement, sans modifier l'image :
Vérifiez avec docker exec conteneur id ou docker exec conteneur whoami.
Conséquences pratiques
- Un utilisateur ordinaire ne peut pas écrire partout : un dossier de données monté doit lui appartenir ou être accessible en écriture.
- Privilégiez les ports supérieurs à 1024 (par exemple 8080) dans le conteneur.
- Certaines images officielles (nginx, par exemple) démarrent en administrateur pour des raisons de fonctionnement : consultez leur documentation avant de leur imposer
--user.
3. Système de fichiers en lecture seule¶
Si le système de fichiers du conteneur est en lecture seule, un attaquant qui prend le contrôle de l'application ne peut ni modifier ses programmes, ni y déposer des fichiers. L'option est --read-only :
Une application a presque toujours besoin d'écrire quelque part (fichiers temporaires, journaux, cache). On autorise l'écriture uniquement aux endroits nécessaires, avec les montages du chapitre 5 :
| Besoin | Solution |
|---|---|
| Fichiers temporaires | --tmpfs /tmp (en mémoire) |
| Données à conserver | Volume nommé monté à l'endroit voulu |
docker run -d --name web --read-only --tmpfs /tmp --tmpfs /var/cache/nginx --tmpfs /var/run nginx:1.27-alpine
Quand une application échoue avec le message Read-only file system, ses journaux indiquent le chemin où elle essaie d'écrire : c'est l'endroit à monter en écriture. Dans un fichier Compose, les clés correspondantes sont read_only: true et tmpfs:.
4. Les capabilities¶
4.1 Principe¶
Sous Linux, les privilèges de l'administrateur sont découpés en capabilities : chacune autorise une action précise (se lier à un port inférieur à 1024, changer le propriétaire d'un fichier, utiliser des sockets réseau bruts…). Docker lance les conteneurs avec un sous-ensemble réduit de ces capabilities, mais ce sous-ensemble reste souvent plus large que nécessaire.
| Capability | Autorise |
|---|---|
NET_BIND_SERVICE |
Écouter sur un port inférieur à 1024 |
CHOWN |
Changer le propriétaire de fichiers |
SETUID, SETGID |
Changer d'identité d'utilisateur ou de groupe |
NET_RAW |
Utiliser des sockets réseau bruts (ping, par exemple) |
SYS_ADMIN |
Un grand nombre d'opérations d'administration : très dangereuse |
4.2 Retirer tout, puis ajouter le nécessaire¶
La méthode du moindre privilège : retirer toutes les capabilities, puis ajouter seulement celles dont l'application a réellement besoin.
Si l'application ne démarre plus, ses journaux indiquent l'opération refusée : on ajoute alors la capability correspondante, une à la fois. Dans un fichier Compose : cap_drop: [ALL] et cap_add: [...].
Pour consulter la configuration d'un conteneur :
4.3 Deux options à connaître¶
| Option | Effet |
|---|---|
--security-opt no-new-privileges |
Empêche un processus du conteneur d'obtenir de nouveaux privilèges (par exemple via un programme à bit setuid) |
--privileged |
Donne toutes les capabilities et l'accès aux périphériques de l'hôte : l'isolation est quasiment supprimée |
N'utilisez jamais --privileged par commodité
Un conteneur --privileged peut quasiment tout faire sur l'hôte. Comme l'accès au socket du moteur (/var/run/docker.sock) monté dans un conteneur, c'est un accès administrateur à la machine. Si une documentation vous le suggère, cherchez d'abord la capability précise qui est nécessaire.
5. Gérer les secrets¶
Un secret est une donnée sensible : mot de passe, jeton, clé privée. Vous avez déjà vu que les placer dans une variable d'environnement n'est pas satisfaisant.
5.1 Où ne pas mettre un secret¶
| Emplacement | Pourquoi c'est dangereux |
|---|---|
Dans le code ou dans un fichier copié dans l'image (COPY .env) |
Il fait partie de l'image et de ses couches : quiconque obtient l'image l'obtient |
Instruction ENV ou ARG du Dockerfile |
Visible avec docker history et docker inspect |
Variable d'environnement au lancement (-e MOT_DE_PASSE=...) |
Visible avec docker inspect, dans l'historique des commandes, et souvent dans les journaux d'erreur |
| Dépôt Git | Il y reste, même après suppression (chapitre 7 : .env non versionné) |
Un secret publié est un secret perdu
Si un secret a été publié par erreur (image, dépôt Git), il faut le révoquer et le remplacer, pas seulement le supprimer de l'endroit où il apparaît.
5.2 Fournir un secret sous forme de fichier¶
La bonne pratique : le secret est un fichier, monté dans le conteneur en lecture seule, que l'application lit. Beaucoup d'images officielles le permettent, avec des variables qui se terminent par _FILE : par exemple, l'image postgres lit le mot de passe dans le fichier indiqué par POSTGRES_PASSWORD_FILE.
docker run -d --name db \
-v "$(pwd)/db_password.txt:/run/secrets/db_password:ro" \
-e POSTGRES_PASSWORD_FILE=/run/secrets/db_password \
postgres:16-alpine
La variable d'environnement ne contient alors que le chemin du fichier, pas le secret.
5.3 Les secrets de Compose¶
Compose propose une section secrets qui monte un fichier dans /run/secrets/<nom> :
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txt
Le fichier db_password.txt est exclu du dépôt Git (.gitignore), comme le fichier .env. Au chapitre 12, vous retrouverez le même principe avec les secrets de Swarm, stockés chiffrés dans le cluster.
5.4 Secrets de construction¶
Quand une instruction du Dockerfile a besoin d'un secret pendant la construction (télécharger un paquet depuis un dépôt privé, par exemple), BuildKit (chapitre 9) fournit un secret temporaire qui n'est pas enregistré dans l'image :
Le secret n'est disponible que pendant cette instruction. Contrairement à un ARG, il n'apparaît pas dans docker history.
Les limites
Un secret monté en fichier reste lisible par l'administrateur de la machine et par le processus de l'application. Il faut aussi prévoir sa rotation (le renouveler régulièrement). Un gestionnaire de secrets dédié dépasse le cadre de ce cours.
6. Analyser les vulnérabilités d'une image¶
6.1 Principe¶
Une image contient des paquets (système, bibliothèques). Des failles de sécurité y sont découvertes en continu : elles sont répertoriées sous un identifiant CVE (par exemple CVE-2024-12345) avec une sévérité. Un scanner de vulnérabilités compare les paquets de l'image à une base de failles connues et produit un rapport.
| Information du rapport | Signification |
|---|---|
| Identifiant CVE | Référence publique de la faille |
| Paquet concerné | Le composant vulnérable (nom, version installée) |
| Sévérité | CRITICAL, HIGH, MEDIUM, LOW |
| Version corrigée (fixed version) | La version qui corrige la faille, si elle existe |
Plusieurs outils existent (Trivy, Docker Scout, Grype…). Vous utiliserez Trivy au Lab 10.2, exécuté lui-même dans un conteneur.
6.2 Interpréter et corriger¶
flowchart LR
A["Analyse de l'image"] --> B["Rapport<br/>CVE, paquet, sévérité"]
B --> C{"Version corrigée<br/>disponible ?"}
C -- "oui" --> D["Mettre à jour la base ou le paquet,<br/>reconstruire"]
C -- "non" --> E["Évaluer le risque,<br/>changer d'image si possible"]
D --> F["Réanalyser"]
E --> F
Les actions de correction les plus efficaces sont celles que vous connaissez déjà :
| Action | Effet |
|---|---|
| Passer à une version plus récente de l'image de base | Reprend les correctifs de la distribution |
Choisir une image plus petite (slim, alpine, multi-stage, scratch) |
Moins de paquets, donc moins de failles possibles (chapitre 9) |
| Reconstruire régulièrement | Récupère les mises à jour de l'image de base |
| Retirer les outils inutiles de l'image finale | Supprime les composants qui portent des failles |
Lire un rapport avec discernement
- Un rapport sans faille ne prouve pas que l'image est sûre : il ne couvre que les failles connues.
- Une faille n'est pas toujours exploitable dans votre contexte ; mais ne l'écartez que si vous savez pourquoi.
- Certaines failles n'ont pas encore de correctif : on note le risque et on le réévalue.
- Priorisez : sévérité
CRITICALetHIGHavec correctif disponible d'abord.
Au chapitre 13, l'analyse sera intégrée à la chaîne de livraison.
7. Un conteneur durci : récapitulatif¶
| Par défaut | Durci |
|---|---|
| Exécution en root | USER dans l'image ou --user |
| Système de fichiers modifiable | --read-only + --tmpfs ou volumes aux endroits nécessaires |
| Capabilities par défaut | --cap-drop ALL puis --cap-add du strict nécessaire |
| Élévation de privilèges possible | --security-opt no-new-privileges |
| Secret en variable d'environnement | Secret en fichier (/run/secrets/...) |
| Image non analysée | Analyse de vulnérabilités avant publication |
| Ports et réseaux ouverts | Réseaux cloisonnés, un seul point d'entrée (chapitre 6) |
| Ressources illimitées | --memory, --cpus (chapitre 8) |
Exemple de commande qui combine plusieurs de ces leviers :
docker run -d --name web \
--user 10001:10001 \
--read-only --tmpfs /tmp \
--cap-drop ALL \
--security-opt no-new-privileges \
--memory 128m --cpus 0.5 \
monapp:1.0
À retenir
- Un conteneur partage le noyau de l'hôte : son isolation est moins forte que celle d'une machine virtuelle.
- Appliquez le moindre privilège : utilisateur ordinaire (
USER), système de fichiers en lecture seule, capabilities réduites,no-new-privileges. --privilegedet le montage du socket Docker donnent un accès administrateur à l'hôte.- Un secret ne va ni dans l'image, ni dans un
ENVouARG, ni dans Git ; on le fournit en fichier monté en lecture seule. - Un scanner compare les paquets d'une image aux failles connues ; on corrige en mettant à jour, en réduisant l'image et en reconstruisant, puis on réanalyse.
- Un rapport vide ne prouve pas l'absence de failles.
Pour vérifier votre compréhension¶
Pourquoi est-il préférable de ne pas exécuter une application en root dans un conteneur ?
Parce qu'un conteneur partage le noyau de l'hôte : si l'application est compromise ou le conteneur mal configuré, un processus root dispose de droits maximaux pour dépasser les limites du conteneur. Un utilisateur ordinaire limite les dégâts.
Une application échoue avec Read-only file system après l'ajout de --read-only. Que faites-vous ?
On lit le chemin indiqué dans les journaux et on autorise l'écriture uniquement à cet endroit, par exemple avec --tmpfs pour des fichiers temporaires ou un volume pour des données. On ne retire pas --read-only.
Pourquoi --cap-drop ALL suivi de --cap-add vaut-il mieux que laisser les capabilities par défaut ?
Parce que l'on part de zéro et que l'on n'ajoute que les privilèges dont l'application a prouvé avoir besoin : tout le reste est interdit, ce qui réduit ce qu'un attaquant peut faire.
Pourquoi un mot de passe passé avec ENV dans le Dockerfile reste-t-il dangereux même si on le supprime ensuite ?
Il est enregistré dans les couches de l'image et dans ses métadonnées, visibles avec docker history et docker inspect. Une suppression ultérieure ne l'enlève pas des couches existantes. Un secret exposé doit être révoqué.
Votre rapport d'analyse n'affiche plus de faille critique. Pouvez-vous conclure que l'image est sûre ?
Non. Le rapport ne couvre que les failles connues de la base utilisée au moment de l'analyse. Il faut réanalyser régulièrement et conserver les autres protections (moindre privilège, réseau cloisonné, mises à jour).