Lab 10.1 : Durcir un conteneur¶
| Durée | 75 min |
| Niveau | Semi-guidé |
| Prérequis | Lab 9.2 : Réduire et comparer des images (application Flask du dossier tp-compare), cours du chapitre 10 |
| Acquis visés | AA7 |
Objectifs
- Constater les droits d'un conteneur lancé « par défaut »
- Exécuter une application avec un utilisateur non privilégié
- Utiliser un système de fichiers en lecture seule et ajouter les montages en écriture strictement nécessaires
- Retirer les capabilities d'un conteneur et ne rajouter que celles qui sont indispensables
- Fournir un mot de passe sous forme de fichier plutôt que de variable d'environnement
- Vérifier le durcissement avec des commandes de contrôle
Contexte et schéma¶
Vous partez d'un conteneur qui fonctionne mais qui a tous les droits par défaut, puis vous le durcissez levier par levier, en testant l'effet de chacun. Dans ce lab, seules les commandes clés vous sont données : à vous de les adapter et de déduire le reste à l'aide du cours.
flowchart LR
A["Conteneur par défaut<br/>root, écriture libre,<br/>capabilities par défaut"] --> B["Utilisateur<br/>non root"]
B --> C["Lecture seule<br/>+ tmpfs"]
C --> D["Capabilities<br/>réduites"]
D --> E["Secret en fichier"]
E --> F["Conteneur durci<br/>et vérifié"]
Selon votre option
- Sous PowerShell, utilisez
curl.exe, écrivez les commandes sur une seule ligne et remplacez$(pwd)par${PWD}. - Les commandes de contrôle (
docker exec,docker inspect) fonctionnent de la même manière avec les trois options.
Étapes¶
Étape 1 : Préparer l'application et constater la situation initiale¶
- Objectif : disposer d'une image « de départ » et mesurer ses droits.
- Indices :
- Créez un dossier
tp-secureet copiez-yapp.pyetrequirements.txtdu Lab 9.2. - Écrivez un
Dockerfile(à partir depython:3.12-slim, ordre optimisé du chapitre 3, port 5000) et construisez l'imagesecure:1.0. - Lancez un conteneur
appsur le port 5000 de l'hôte, sans aucune option de sécurité, puis testez aveccurl. - Relevez l'identité du processus et la possibilité d'écrire dans l'image :
- Créez un dossier
Point de contrôle
-
idindique l'utilisateurroot(uid 0) - L'écriture dans
/appréussit - Vous avez compris que ce conteneur dispose de tous les droits par défaut
Étape 2 : Exécuter avec un utilisateur non privilégié¶
- Objectif : l'application ne s'exécute plus en root.
- Indices :
- Écrivez
Dockerfile.useren créant un utilisateurappuserd'identifiant 10001 et en l'activant avecUSER, après l'installation des dépendances (section 2.2 du cours, onglet Debian). Les fichiers de l'application doivent appartenir à cet utilisateur (--chown). - Construisez
secure:2.0avec-f Dockerfile.user, lancez-la (en supprimant d'abord l'ancien conteneur) et testez. - Refaites les deux commandes de l'étape 1.
- Testez aussi l'imposition de l'utilisateur au lancement sur l'image d'origine :
docker run --rm --user 10001:10001 secure:1.0 id.
- Écrivez
Point de contrôle
-
curlaffiche le message de l'application -
idindique l'uid 10001 - L'écriture dans
/appest refusée (Permission denied)
Étape 3 : Système de fichiers en lecture seule¶
- Objectif : interdire toute écriture dans le conteneur, sauf aux endroits nécessaires.
- Indices :
- Relancez
secure:2.0avec l'option--read-only, puis testez aveccurl. - Essayez d'écrire dans
/tmp:docker exec app sh -c "echo test > /tmp/f.txt". Ajoutez--tmpfs /tmpet recommencez. - Appliquez ensuite
--read-onlyà un serveur qui a besoin d'écrire : lanceznginx:1.27-alpineavec--read-only(conteneurweb, port 8080) et lisez ses journaux. Ajoutez au fur et à mesure les montages--tmpfsqui correspondent aux chemins signalés, jusqu'à ce que le serveur démarre (section 3 du cours).
- Relancez
Point de contrôle
- L'application Flask répond avec
--read-only; l'écriture dans/tmpn'est possible qu'avec--tmpfs /tmp - Vous avez relevé dans les journaux de nginx les chemins signalés en lecture seule
- nginx fonctionne avec
--read-onlyet la liste minimale de--tmpfsque vous avez déterminée -
curl http://localhost:8080affiche la page de nginx
Étape 4 : Réduire les capabilities¶
- Objectif : voir l'effet des capabilities, puis ne garder que le nécessaire.
- Indices :
- Comparez les capabilities effectives d'un conteneur par défaut et d'un conteneur sans capabilities :
docker run --rm alpine:3.20 grep CapEff /proc/self/status
docker run --rm --cap-drop ALL alpine:3.20 grep CapEff /proc/self/status
- Testez une action qui exige la capability `CHOWN` (changer le propriétaire d'un fichier), avec et sans `--cap-drop ALL`, puis avec `--cap-drop ALL --cap-add CHOWN` :
- Lancez maintenant nginx (avec ses `--tmpfs` et `--read-only` de l'étape 3) avec `--cap-drop ALL` : lisez les journaux, puis ajoutez **une capability à la fois** (`--cap-add`) jusqu'à ce que le serveur démarre. Notez la liste minimale.
Point de contrôle
-
CapEffest non nul par défaut et nul avec--cap-drop ALL -
chownéchoue sans la capability et réussit avec--cap-add CHOWN - Vous avez déterminé la liste minimale de capabilities de nginx, avec les messages qui vous y ont conduit
-
docker inspect -f '{{.HostConfig.CapDrop}} {{.HostConfig.CapAdd}}' webaffiche votre configuration
Étape 5 : Assembler un conteneur durci¶
- Objectif : combiner les leviers pour l'application Flask.
- Indices : lancez
secure:2.0(conteneurapp-durci, port 5001 de l'hôte) avec : l'utilisateur non privilégié, le système de fichiers en lecture seule avec/tmpentmpfs,--cap-drop ALL,--security-opt no-new-privileges, et des limites de mémoire et de processeur de votre choix (chapitre 8). Testez aveccurl, puis vérifiez la configuration :
docker inspect -f '{{.HostConfig.ReadonlyRootfs}} {{.HostConfig.CapDrop}} {{.HostConfig.SecurityOpt}} {{.Config.User}}' app-durci
Point de contrôle
- L'application répond sur le port 5001
-
docker inspectindiquetrue,[ALL],[no-new-privileges](ou équivalent) et l'utilisateur configuré - Le conteneur ne peut ni écrire dans
/app, ni obtenir de privilèges
Étape 6 : Fournir un mot de passe sous forme de fichier¶
- Objectif : ne plus passer le mot de passe dans une variable d'environnement.
- Indices :
- Mauvaise méthode : lancez un conteneur
db-env(postgres:16-alpine,--rm) avec-e POSTGRES_PASSWORD=motdepasse-tp, puis affichez ses variables avecdocker inspect -f '{{.Config.Env}}' db-env. - Bonne méthode : créez un fichier
db_password.txtcontenantmotdepasse-tp(sans saut de ligne superflu). Sous PowerShell, utilisezSet-Content -NoNewline -Encoding ascii db_password.txt "motdepasse-tp". Lancez un conteneurdb-file(postgres:16-alpine,--rm) qui monte ce fichier en lecture seule sur/run/secrets/db_passwordet utilisePOSTGRES_PASSWORD_FILE(section 5.2 du cours). - Vérifiez avec
docker inspectque le mot de passe n'apparaît plus, et que la base démarre (docker logs db-file,docker exec db-file pg_isready). - Ajoutez
db_password.txtà un fichier.gitignore.
- Mauvaise méthode : lancez un conteneur
Point de contrôle
- Avec
-e, le mot de passe apparaît en clair dansdocker inspect - Avec le fichier,
docker inspectn'affiche que le chemin du fichier - La base démarre avec le mot de passe lu dans le fichier
-
db_password.txtest dans.gitignore
Étape 7 (facultative) : Durcir un service Compose¶
- Objectif : exprimer le durcissement dans un fichier Compose.
- Indices : créez un fichier
compose.yamlpour l'application Flask avec les clésread_only,tmpfs,cap_drop,security_opt,mem_limitetcpus, puis une sectionsecretspour le mot de passe de la base (sections 3 à 5 du cours). Vérifiez avecdocker compose config, puis avecdocker inspect.
Point de contrôle
- La pile démarre avec le durcissement exprimé dans le fichier
-
docker inspectconfirme chaque option
Étape 8 : Dresser la fiche de contrôle¶
- Objectif : savoir prouver chaque protection.
- Indices : complétez le tableau avec la commande que vous avez réellement utilisée pour vérifier chaque point sur
app-durciou surdb-file.
| Protection | Commande de vérification | Résultat attendu |
|---|---|---|
| Utilisateur non root | ||
| Système de fichiers en lecture seule | ||
| Capabilities retirées | ||
| Pas d'élévation de privilèges | ||
| Secret absent des variables d'environnement |
Point de contrôle
- Chaque ligne contient une commande que vous avez exécutée pendant ce lab
Questions de réflexion¶
Pourquoi l'écriture dans /app est-elle refusée après l'étape 2 alors que l'application fonctionne ?
Le conteneur s'exécute avec l'utilisateur 10001, qui n'est pas propriétaire de /app et n'a pas le droit d'y créer de fichier. L'application n'a besoin que de lire son code, pas d'y écrire.
Pourquoi retire-t-on toutes les capabilities puis ajoute-t-on celles qui sont nécessaires, plutôt que de retirer les capabilities dangereuses une par une ?
Une liste d'exclusion est incomplète par construction : on risque d'oublier une capability dangereuse. Une liste d'autorisation part de zéro et ne contient que ce qui a été justifié.
Pourquoi un secret passé par -e est-il visible dans docker inspect mais pas un secret monté en fichier ?
Les variables d'environnement font partie de la configuration du conteneur, que docker inspect affiche. Avec un fichier, la configuration ne contient que le chemin ; le contenu reste dans le fichier monté.
Un conteneur durci est-il pour autant invulnérable ?
Non. Le durcissement réduit ce qu'un attaquant peut faire s'il compromet l'application, mais il ne corrige pas ses failles. Il se complète par l'analyse des vulnérabilités des images (Lab 10.2), les mises à jour et un réseau cloisonné.
Dépannage¶
| Symptôme | Pistes |
|---|---|
Permission denied au démarrage de l'application après USER |
Les fichiers de l'application n'appartiennent pas à l'utilisateur : COPY --chown, ou droits sur un dossier monté |
Read-only file system dans les journaux |
Chemin à ouvrir en écriture : ajoutez un --tmpfs (temporaire) ou un volume (données) à cet endroit |
nginx s'arrête avec --cap-drop ALL |
Messages Operation not permitted : repérez l'opération et ajoutez la capability correspondante, une à la fois |
docker: invalid reference format ou option ignorée |
Les options doivent précéder le nom de l'image dans docker run |
useradd: command not found ou adduser introuvable |
La commande dépend de l'image de base : useradd sur Debian et -slim, adduser sur Alpine |
password authentication failed ou la base ne démarre pas avec POSTGRES_PASSWORD_FILE |
Chemin du fichier monté ou nom de variable incorrect, ou fichier vide ; vérifiez docker logs db-file |
| Le fichier du mot de passe contient des caractères parasites (PowerShell) | Utilisez Set-Content -NoNewline -Encoding ascii et non la redirection > |
port is already allocated |
Un conteneur d'une étape précédente occupe le port : supprimez-le |
Nettoyage et reprise¶
Certains conteneurs n'existent peut-être plus (--rm) : Docker le signale sans conséquence. Supprimez le fichier db_password.txt si vous n'en avez plus besoin. Le Lab 10.2 réutilise l'application de ce lab.
Retour au chapitre¶
Chapitre 10 : Sécurité des conteneurs · Lab suivant : Lab 10.2 : Analyser et corriger une image