Docker - Builds multi-étapes et gestion des secrets
Après avoir maîtrisé les fondamentaux de la sécurité Docker, cette quête explore des techniques avancées pour créer des conteneurs à la fois sécurisés et optimisés.
Les builds multi-étapes, la gestion des secrets et l'optimisation des images sont des pratiques essentielles pour réduire drastiquement la surface d'attaque des applications conteneurisées.

⚙️ Pré-requis
Avant de commencer cette quête, il est nécessaire de :
- Comprendre les bases de Docker (images, conteneurs, volumes)
- Savoir créer des Dockerfiles simples
- Disposer d'un environnement Linux avec Docker installé
Et avoir réalisé les quêtes :
🎯 Objectifs :
✅ Maîtriser la technique des builds multi-étapes pour sécuriser les conteneurs ✅ Implémenter des stratégies avancées de gestion des secrets ✅ Optimiser les images Docker pour réduire leur surface d'attaque ✅ Mettre en place un processus de construction d'images reproductible et sécurisé
Les builds multi-étapes représentent une technique avancée permettant de séparer l'environnement de construction de l'environnement d'exécution, réduisant ainsi considérablement la taille de l'image finale et sa surface d'attaque.
Principe des builds multi-étapes
Un build multi-étapes utilise plusieurs instructions FROM dans un même Dockerfile, chacune commençant une nouvelle étape. Les artefacts peuvent être copiés d'une étape à l'autre, mais seule la dernière étape sera conservée dans l'image finale.
# Étape de build
FROM python:3.11 AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Étape finale
FROM python:3.11-slim
RUN adduser --system --group appuser
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY app.py .
USER appuser
CMD ["python", "app.py"]
Ce Dockerfile illustre le principe des builds multi-étapes et leurs avantages pour la sécurité :
-
Première étape : Utilise une image Python complète (
python:3.11) pour installer les dépendances. Cette image est volumineuse et contient des outils de développement. -
Deuxième étape : Commence avec une image plus légère (
python:3.11-slim) et ne copie que les packages Python installés lors de la première étape, sans inclure tous les outils de développement.
Les avantages de cette approche sont nombreux :
- L'image finale est considérablement plus petite
- Les outils de build potentiellement vulnérables (compilateurs, outils de développement) ne sont pas présents dans l'image finale
- L'attaquant dispose de moins d'outils pour exploiter une éventuelle vulnérabilité
Exemple avancé avec une application Python
Voici un exemple plus complet pour une application web Python utilisant Flask :
# Étape de build
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
# Installation des dépendances de build
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc build-essential && \
pip wheel --no-cache-dir --wheel-dir=/wheels -r requirements.txt && \
rm -rf /var/lib/apt/lists/*
# Étape finale
FROM python:3.11-slim
# Création d'un utilisateur non-root
RUN adduser --system --group --uid 1000 appuser
# Installation des dépendances depuis les wheels pré-compilés
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* && \
rm -rf /wheels
# Configuration de l'application
WORKDIR /app
COPY --chown=appuser:appuser app/ .
USER appuser
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]
Ce Dockerfile avancé apporte plusieurs améliorations supplémentaires :
-
Dans l'étape de build, les dépendances Python sont précompilées en "wheels" grâce à la commande
pip wheel, ce qui accélère l'installation dans l'étape finale. -
Les packages système nécessaires à la compilation (
gcc,build-essential) sont installés uniquement dans l'étape de build, mais absents de l'image finale. -
Dans l'étape finale, les dépendances sont installées à partir des wheels précompilés, sans nécessiter de compilateur.
-
Toutes les opérations qui modifient le système (installation de packages, création d'utilisateur) sont effectuées avant la copie du code application, permettant de tirer parti du cache Docker pour accélérer les builds.
-
L'application est lancée avec Gunicorn, un serveur WSGI plus adapté à la production que le serveur de développement intégré à Flask.
Avantages des builds multi-étapes pour la sécurité
Les builds multi-étapes offrent plusieurs avantages significatifs en termes de sécurité :
-
Réduction de la surface d'attaque : L'image finale ne contient que le strict nécessaire à l'exécution de l'application, sans outils de développement ou fichiers temporaires qui pourraient être exploités par un attaquant.
-
Séparation des préoccupations : Les opérations de build sont isolées de l'environnement d'exécution, ce qui permet d'adopter une approche "construire une fois, déployer partout" plus sécurisée.
-
Images plus petites : Des images plus légères sont plus rapides à télécharger et à déployer, ce qui réduit la fenêtre d'opportunité pour les attaques pendant le déploiement.
-
Moins de vulnérabilités : Avec moins de packages installés dans l'image finale, le nombre potentiel de vulnérabilités est mécaniquement réduit.
-
Facilité d'analyse : Des images plus petites et plus simples sont plus faciles à analyser avec des outils de sécurité comme Trivy.
En résumé, les builds multi-étapes constituent un outil puissant pour appliquer le principe du moindre privilège aux conteneurs Docker, en ne conservant que les composants strictement nécessaires à l'exécution de l'application.
Scénarios de builds multi-étapes par type d'application
Les builds multi-étapes peuvent être adaptés à différents types d'applications :
Pour une application Python avec dépendances complexes :
# Build stage pour les dépendances avec compilateur
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
# Installation des outils de compilation nécessaires
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc python3-dev libffi-dev && \
pip wheel --no-cache-dir --wheel-dir=/wheels -r requirements.txt
# Stage final sans compilateur
FROM python:3.11-slim
WORKDIR /app
RUN adduser --system --group appuser
# Copie des wheels précompilés
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* && \
rm -rf /wheels
COPY --chown=appuser:appuser app/ .
USER appuser
CMD ["python", "app.py"]
Pour une application avec assets web à compiler :
# Stage de build pour les assets frontend
FROM node:18-alpine AS frontend-builder
WORKDIR /build
COPY package*.json ./
RUN npm ci
COPY frontend/ ./frontend/
RUN npm run build
# Stage de build pour l'application Python
FROM python:3.11-slim AS backend-builder
WORKDIR /build
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir=/wheels -r requirements.txt
# Stage final
FROM python:3.11-slim
WORKDIR /app
RUN adduser --system --group appuser
# Installation des dépendances Python
COPY --from=backend-builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* && \
rm -rf /wheels
# Copie des assets frontend compilés
COPY --from=frontend-builder /build/frontend/dist /app/static
# Copie de l'application backend
COPY --chown=appuser:appuser app/ .
USER appuser
CMD ["gunicorn", "app:app"]
Ces exemples montrent comment les builds multi-étapes peuvent être adaptés à différents scénarios, tout en maintenant le principe de séparation entre environnement de build et environnement d'exécution.
La gestion des secrets (clés API, mots de passe, certificats, etc.) dans les Dockerfiles est un aspect critique de la sécurité des conteneurs. Une mauvaise pratique dans ce domaine peut exposer des informations sensibles et compromettre la sécurité de l'ensemble du système.
Inclure des secrets dans le Dockerfile
TRÈS MAUVAISE PRATIQUE - NE JAMAIS FAIRE CECI
FROM python:3.11-alpine3.19
ENV API_KEY="50c6168d-ea87-4c3b-8a29-f5e47d4c2192"
ENV DB_PASSWORD="s3cr3t_p4ssw0rd!"
WORKDIR /app
COPY app.py .
CMD ["python", "app.py"]
Ce Dockerfile contient plusieurs erreurs critiques de sécurité :
-
Les secrets (API_KEY et DB_PASSWORD) sont directement inclus dans le Dockerfile, ce qui signifie qu'ils seront visibles :
- Dans l'historique des images (
docker history) - Dans le code source si le Dockerfile est versionné
- Pour quiconque a accès au registre d'images
- Dans l'historique des images (
-
Ces secrets sont définis comme des variables d'environnement, ce qui les rend accessibles à tous les processus s'exécutant dans le conteneur et visibles via
docker inspect.
Approches recommandées pour la gestion des secrets
Il existe plusieurs méthodes sécurisées pour gérer les secrets dans les conteneurs Docker :
1. Injection au moment de l'exécution
FROM python:3.11-alpine3.19
WORKDIR /app
COPY app.py .
CMD ["python", "app.py"]
docker run -e API_KEY="50c6168d-ea87-4c3b-8a29-f5e47d4c2192" -e DB_PASSWORD="s3cr3t_p4ssw0rd!" my-app
Cette approche injecte les secrets au moment de l'exécution du conteneur, évitant qu'ils ne soient stockés dans l'image. Cela convient bien aux déploiements manuels ou aux environnements de développement.
2. Utilisation de fichiers d'environnement
Avec cette méthode, on charge les variables d'environnement à partir d'un fichier.
Cela qui facilite la gestion des secrets et évite qu'ils n'apparaissent dans l'historique des commandes.
cat .env
API_KEY=50c6168d-ea87-4c3b-8a29-f5e47d4c2192
DB_PASSWORD=s3cr3t_p4ssw0rd!
docker run --env-file .env my-app
3. Docker Swarm Secrets ou Kubernetes Secrets
Pour les environnements de production, utilisez les mécanismes de secrets natifs de la plateforme d'orchestration :
Docker Swarm :
echo "s3cr3t_p4ssw0rd!" | docker secret create db_password -
docker service create --secret db_password --name my-app my-app-image
Kubernetes :
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
api-key: NTBjNjE2OGQtZWE4Ny00YzNiLThhMjktZjVlNDdkNGMyMTky
db-password: czNjcjN0X3A0c3N3MHJkIQ==
Ces mécanismes fournissent un chiffrement au repos et des contrôles d'accès granulaires pour les informations sensibles.
4. BuildKit et les secrets temporaires
Docker BuildKit permet d'utiliser des secrets lors de la construction de l'image sans qu'ils ne soient inclus dans le résultat final :
# Nécessite Docker 18.09 ou plus récent avec BuildKit activé
FROM python:3.11-alpine3.19
WORKDIR /app
COPY . .
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret | pip install -r requirements.txt
USER 1000
CMD ["python", "app.py"]
DOCKER_BUILDKIT=1 docker build --secret id=mysecret,src=./secret.txt -t myimage .
Cette approche permet d'utiliser des secrets pendant le processus de build (par exemple, pour accéder à des dépôts privés) sans qu'ils ne soient stockés dans l'image finale.
5. Gestionnaires de secrets externes
Pour des environnements d'entreprise, l'intégration avec des gestionnaires de secrets externes offre une sécurité renforcée :
FROM python:3.11-slim
# Installation du client Vault
RUN apt-get update && \
apt-get install -y curl && \
curl -L -o /usr/local/bin/vault https://releases.hashicorp.com/vault/1.12.3/vault_1.12.3_linux_amd64.zip && \
unzip -d /usr/local/bin /usr/local/bin/vault && \
chmod +x /usr/local/bin/vault && \
apt-get remove -y curl unzip && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Script d'initialisation qui récupère les secrets au démarrage
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["python", "app.py"]
Avec un script d'initialisation comme celui-ci :
#!/bin/bash
# entrypoint.sh
set -e
# Récupération des secrets depuis Vault
export API_KEY=$(vault kv get -field=api_key secret/myapp)
export DB_PASSWORD=$(vault kv get -field=db_password secret/myapp)
# Exécution de la commande principale
exec "$@"
Cette approche utilise HashiCorp Vault ou des solutions similaires (AWS Secrets Manager, Azure Key Vault) pour récupérer les secrets au démarrage du conteneur.
En résumé, la gestion des secrets dans Docker doit suivre le principe fondamental suivant : les secrets ne doivent jamais être inclus dans les images Docker. Ils doivent être fournis au moment de l'exécution, de préférence via des mécanismes spécialisés de la plateforme d'orchestration utilisée.
La réduction de la taille des images Docker n'est pas seulement une question de performance, mais aussi un aspect important de la sécurité. Plus une image est volumineuse, plus elle contient potentiellement de composants vulnérables.
Minimiser le nombre de couches
Chaque instruction RUN, COPY et ADD dans un Dockerfile crée une nouvelle couche dans l'image. La réduction du nombre de couches améliore les performances et simplifie l'analyse de sécurité.
Mauvaise pratique - Trop de couches
FROM debian:12-slim
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get install -y python3-pip
RUN pip install flask
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
# Bonne pratique - Combinaison des commandes
FROM debian:12-slim
RUN apt-get update && \
apt-get install -y python3 python3-pip && \
pip install flask && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Dans la version optimisée, toutes les commandes d'installation sont regroupées dans une seule instruction RUN, ce qui :
- Réduit le nombre de couches de 6 à 1
- Assure que les caches temporaires de apt et les listes de packages ne sont présents dans aucune couche
- Simplifie l'historique de l'image, facilitant son analyse
Supprimer les outils et fichiers inutiles
Éliminer les outils de développement, compilateurs et fichiers temporaires qui ne sont pas nécessaires pour l'exécution de l'application :
FROM debian:12-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends \
python3 \
python3-pip \
# Éviter d'installer des packages non essentiels
# gcc \
# build-essential \
&& pip install --no-cache-dir flask \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
# Supprimer les caches pip et les fichiers temporaires
&& rm -rf /root/.cache /tmp/*
Ce Dockerfile utilise plusieurs techniques pour minimiser la taille de l'image :
--no-install-recommendsévite l'installation de packages "recommandés" mais non essentiels--no-cache-dirempêche pip de conserver des caches des packages téléchargés- Nettoyage agressif des fichiers temporaires, caches et listes de packages
Utilisation stratégique du .dockerignore
Le fichier .dockerignore permet d'exclure des fichiers du contexte de build, réduisant la taille et évitant d'inclure des informations sensibles :
# Contrôle de version
.git
.gitignore
.svn
# Environnements de développement
.idea
.vscode
*.sublime-project
*.sublime-workspace
# Fichiers de build et caches
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
*.egg-info/
# Tests
tests/
test/
pytest_cache/
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
# Environnement
.env
.venv
env/
venv/
ENV/
# Documentation
docs/
*.md
*.rst
# Fichiers potentiellement sensibles
secrets/
credentials/
*.pem
*.key
Ce fichier .dockerignore exhaustif exclut de nombreux types de fichiers qui ne devraient pas être inclus dans une image Docker de production :
- Fichiers de contrôle de version
- Fichiers de configuration d'IDE
- Caches de compilation
- Tests et couverture
- Environnements virtuels
- Documentation
- Fichiers potentiellement sensibles
Utiliser les options de build pour la reproductibilité
# Dockerfile avec paramétrage par ARG
FROM python:3.11-alpine3.19
ARG PYTHON_VERSION=3.11.7
ARG ALPINE_VERSION=3.19
ARG APP_VERSION=1.2.3
LABEL org.opencontainers.image.version="${APP_VERSION}"
LABEL org.opencontainers.image.created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Installation des dépendances avec versions fixées
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Configuration de l'application avec la version spécifique
RUN wget -O app.tar.gz https://example.com/app-${APP_VERSION}.tar.gz && \
tar -xzf app.tar.gz && \
rm app.tar.gz
USER 1000
CMD ["python", "app.py"]
Ce Dockerfile utilise des arguments de build (ARG) pour centraliser et paramétrer les versions, ce qui :
- Facilite la mise à jour coordonnée des versions
- Améliore la traçabilité via les labels standardisés
- Permet de reconstruire exactement la même image si nécessaire
Vérification des sommes de contrôle (checksums)
Pour les téléchargements effectués pendant le build, vérifiez toujours l'intégrité des fichiers :
FROM alpine:3.19
ARG APP_VERSION=1.2.3
ARG APP_CHECKSUM="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
# Téléchargement avec vérification d'intégrité
RUN wget -O app.tar.gz https://example.com/app-${APP_VERSION}.tar.gz && \
echo "${APP_CHECKSUM} app.tar.gz" | sha256sum -c - && \
tar -xzf app.tar.gz && \
rm app.tar.gz
Cette vérification garantit que le fichier téléchargé n'a pas été modifié ou corrompu, ce qui pourrait indiquer une attaque de type "supply chain" (compromission de la chaîne d'approvisionnement).
Ces techniques contribuent non seulement à des images plus petites et plus efficaces, mais aussi plus sécurisées en réduisant la surface d'attaque potentielle et en renforçant la traçabilité et la reproductibilité des builds. runtime Python et les bibliothèques système essentielles, sans shell ni utilitaires de base comme ls ou cat. Cette configuration minimale réduit considérablement les possibilités pour un attaquant d'explorer ou d'exploiter le système en cas de compromission.
L'instruction COPY --from=python:3.11-slim utilise une technique de build multi-étages pour copier uniquement les packages Python nécessaires sans inclure les outils de développement. L'option --chown=nonroot:nonroot garantit que les fichiers appartiennent à un utilisateur non privilégié, et la directive USER nonroot assure que l'application s'exécute sans privilèges root.
Spécifier des versions précises (tag pinning)
Ne jamais utiliser de tags flottants comme latest ou stable pour les environnements de production, car ils peuvent changer sans préavis.
# À éviter
FROM python:latest
# Préférer
FROM python:3.11.7-alpine3.19
Cette pratique, appelée "tag pinning", est essentielle pour garantir la reproductibilité et la prévisibilité des builds. En spécifiant une version exacte comme 3.11.7-alpine3.19, on s'assure que chaque reconstruction de l'image utilisera exactement la même base, évitant ainsi l'introduction accidentelle de nouvelles vulnérabilités ou d'incompatibilités.
Sans version fixe, l'instruction FROM python:latest pourrait télécharger une image complètement différente lors de chaque build, menant potentiellement à des problèmes difficiles à diagnostiquer ou à des vulnérabilités non anticipées.
Mettre à jour les paquets du système
Lors de la création de l'image, il est crucial de s'assurer que tous les paquets sont mis à jour pour inclure les derniers correctifs de sécurité.
# Pour les images basées sur Debian/Ubuntu
FROM debian:12-slim
RUN apt-get update && \
apt-get upgrade -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Pour les images basées sur Alpine
FROM alpine:3.19
RUN apk update && \
apk upgrade && \
rm -rf /var/cache/apk/*
Ces commandes mettent à jour tous les packages du système d'exploitation avec les derniers correctifs de sécurité disponibles. Les commandes de nettoyage (apt-get clean, rm -rf /var/lib/apt/lists/*, rm -rf /var/cache/apk/*) sont également importantes car elles réduisent la taille de l'image en supprimant les fichiers temporaires et les caches.
Une bonne pratique consiste à regrouper ces commandes dans une seule instruction RUN pour minimiser le nombre de couches dans l'image finale, réduisant ainsi sa taille et améliorant les performances.
Utiliser un utilisateur non-root
La création et l'utilisation d'un utilisateur spécifique avec des privilèges limités est une mesure de sécurité fondamentale.
FROM python:3.11-alpine3.19
RUN addgroup -g 1000 appgroup && \
adduser -u 1000 -G appgroup -D appuser
WORKDIR /app
COPY --chown=appuser:appgroup requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=appuser:appgroup app.py .
USER appuser
CMD ["python", "app.py"]
Ce Dockerfile crée un utilisateur dédié (appuser) et un groupe (appgroup) avec des IDs spécifiques (1000), puis configure le conteneur pour exécuter l'application sous cette identité. Les options utilisées dans la commande adduser sont spécifiques à Alpine Linux :
-u 1000: Définit l'ID utilisateur numérique-G appgroup: Assigne l'utilisateur au groupe spécifié-D: Crée un utilisateur sans mot de passe (approprié pour les conteneurs)
L'instruction COPY --chown=appuser:appgroup garantit que les fichiers copiés dans le conteneur appartiennent à l'utilisateur non-root, plutôt qu'à root par défaut. Enfin, USER appuser change l'utilisateur pour toutes les instructions suivantes et pour le processus principal du conteneur.
Cette approche limite considérablement les dommages potentiels en cas de compromission de l'application, car l'attaquant n'aurait accès qu'aux ressources disponibles pour cet utilisateur non privilégié.
Limiter le contenu de l'image
N'inclure que ce qui est nécessaire pour exécuter l'application :
FROM python:3.11-alpine3.19
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
# Ne pas inclure de fichiers de développement, tests, documentation, etc.
# COPY tests/ . # À éviter dans l'image de production
USER 1000
CMD ["python", "app.py"]
Ce Dockerfile adopte une approche minimaliste en ne copiant que les fichiers strictement nécessaires à l'exécution de l'application. La ligne commentée montre un exemple de ce qu'il faut éviter : inclure des fichiers de test, de documentation ou de développement dans l'image de production.
L'option --no-cache-dir pour pip est également importante car elle évite de stocker le cache des packages téléchargés, réduisant ainsi la taille de l'image finale.
Pour aller plus loin dans cette approche, il est recommandé d'utiliser un fichier .dockerignore qui fonctionne comme un .gitignore pour exclure automatiquement certains fichiers et répertoires du contexte de build :
.git
.github
tests/
docs/
*.md
*.log
__pycache__/
*.py[cod]
*$py.class
Ce fichier .dockerignore exclut les fichiers et répertoires inutiles pour l'exécution de l'application, réduisant la taille du contexte de build et évitant d'inclure potentiellement des informations sensibles dans l'image.
- 1
Étape 1 : Analyse d'un Dockerfile non sécurisé
Commençons par créer et analyser un Dockerfile présentant des problèmes de sécurité :
bashmkdir -p ~/dockerfile-security && cd ~/dockerfile-securitybashcat > Dockerfile.insecure << 'EOF' FROM ubuntu:latest RUN apt-get update RUN apt-get install -y python3 python3-pip curl wget vim RUN pip3 install flask requests urllib3 sqlalchemy ENV DB_PASSWORD="devpassword123" ENV API_KEY="29a1b42ce7ed59d3" COPY . /app/ WORKDIR /app EXPOSE 5000 CMD ["python3", "app.py"] EOFbashcat > app.py << 'EOF' from flask import Flask import os app = Flask(__name__) @app.route('/') def hello(): return "Hello, World! API Key: " + os.environ.get('API_KEY') if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) EOFCette configuration présente plusieurs problèmes de sécurité :
- Image de base avec tag latest non spécifique
- Installation d'outils non nécessaires (vim, wget, curl)
- Secrets en clair dans le Dockerfile (DB_PASSWORD, API_KEY)
- Exécution en tant que root
- Copie de tout le répertoire courant qui pourrait contenir des fichiers sensibles
- L'application expose un secret (API_KEY) dans sa réponse
- 2
Étape 2 : Création d'un Dockerfile sécurisé avec build multi-étapes
Créons maintenant une version améliorée utilisant un build multi-étapes :
bash$ cat > Dockerfile.secure << 'EOF' FROM python:3.11-slim AS builder WORKDIR /build COPY requirements.txt . RUN pip wheel --no-cache-dir --wheel-dir=/wheels -r requirements.txt FROM python:3.11-slim RUN groupadd -g 1000 appuser && \ useradd -u 1000 -g appuser -s /bin/bash -m appuser COPY --from=builder /wheels /wheels RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* && \ rm -rf /wheels WORKDIR /app COPY --chown=appuser:appuser app.py . USER appuser EXPOSE 5000 CMD ["python", "app.py"] EOF$ cat > requirements.txt << 'EOF' flask==2.3.3 requests==2.31.0 EOF
$ cat > app.py << 'EOF' from flask import Flask import os
app = Flask(name)
@app.route('/') def hello(): return "Hello, World!"
@app.route('/health') def health(): return "OK"
if name == 'main': app.run(host='0.0.0.0', port=5000) EOF
Cette version améliorée implémente plusieurs bonnes pratiques : - Build multi-étapes pour isoler l'environnement de build - Image de base avec version spécifique (python:3.11-slim) - Fichier requirements.txt avec versions fixées - Création et utilisation d'un utilisateur non-root - Copie sélective des fichiers nécessaires uniquement - Pas de secrets dans le Dockerfile - Application qui n'expose plus d'informations sensibles # Étape 3 : Construction et comparaison des images Construisons les deux images pour comparer leurs tailles et caractéristiques : ```bash docker build -t app:insecure -f Dockerfile.insecure . docker build -t app:secure -f Dockerfile.secure . docker images | grep appOn observe généralement que l'image sécurisée est significativement plus petite que l'image non sécurisée.
Étape 4 : Exécution et vérification des utilisateurs
Vérifions maintenant les utilisateurs sous lesquels s'exécutent les conteneurs :
bashdocker run --rm app:insecure id docker run --rm app:secure idLa première commande affiche généralement un utilisateur root (uid=0), tandis que la seconde montre l'utilisateur non-root (uid=1000) que nous avons créé.
Étape 5 : Analyse des couches et de l'historique
Examinons les différences de structure entre les deux images :
bashdocker history app:insecure docker history app:secureL'analyse de l'historique révèle des différences importantes :
- L'image non sécurisée contient davantage de couches
- L'image non sécurisée expose des secrets dans son historique
- L'image sécurisée a une structure plus propre et plus claire
Étape 6 : Déploiement sécurisé avec injection de secrets
Pour un déploiement sécurisé, injectons les secrets au moment de l'exécution :
bashtouch .env echo "DB_PASSWORD=securepass123" >> .env echo "API_KEY=9a72bd91c7396f2b" >> .env docker run --rm --env-file .env app:secure printenv | grep -E 'DB_PASSWORD|API_KEY'Cette approche permet de gérer les secrets séparément de l'image, évitant qu'ils ne soient stockés dans les couches de l'image ou visibles dans l'historique.
true||true|||true
# Quelle est la principale raison de sécurité pour utiliser des builds multi-étapes dans un Dockerfile ?
[] Ils accélèrent le processus de construction des images
[x] Ils permettent de réduire la surface d'attaque en n'incluant que le nécessaire dans l'image finale
[] Ils facilitent la gestion des versions des packages
[] Ils améliorent la compatibilité avec les différentes versions de Docker
# Quelle est la méthode la plus sécurisée pour gérer les secrets avec Docker ?
[] Les stocker dans le Dockerfile avec l'instruction ENV
[] Les coder en dur dans le code source de l'application
[x] Les injecter au moment de l'exécution via des mécanismes comme Docker Swarm Secrets ou Kubernetes Secrets
[] Les stocker dans un volume Docker partagé
# Quelle instruction Dockerfile évite d'inclure des fichiers inutiles ou sensibles dans le contexte de build ?
[] EXCLUDE
[] IGNORE
[x] Utiliser un fichier .dockerignore
[] SENSITIVE_FILES
# Quelle pratique réduit le nombre de couches dans une image Docker ?
[] Séparer chaque commande RUN dans le Dockerfile
[] Utiliser l'instruction LAYER MINIMIZE
[x] Combiner plusieurs commandes RUN avec des opérateurs && et \
[] Utiliser l'instruction COLLAPSE
# Quelle est l'image de base généralement recommandée pour minimiser la surface d'attaque ?
[] ubuntu:latest
[] centos:7
[x] alpine:3.19 ou distroless
[] debian:full
💪 Challenge
Le défi consiste à refactoriser une application web Python conteneurisée en appliquant les meilleures pratiques de sécurité et d'optimisation des Dockerfiles.
🧩 Objectifs
- Transformer un Dockerfile non sécurisé en une version optimisée et sécurisée
- Implémenter un build multi-étapes pour réduire la surface d'attaque
- Mettre en place une gestion sécurisée des secrets
- Optimiser l'image pour réduire sa taille
- Documenter les améliorations de sécurité apportées
⚙️ Fonctionnalités attendues
Le projet doit inclure, dans un dépôt gitlab.com ou github :
-
Dockerfile sécurisé utilisant les techniques vues dans cette quête :
- Build multi-étapes
- Utilisateur non-root
- Versions spécifiques des images de base
- Gestion sécurisée des secrets
- Minimisation de la taille de l'image
-
Fichier .dockerignore excluant les fichiers non nécessaires ou sensibles
-
Script de déploiement montrant comment exécuter le conteneur de manière sécurisée avec injection des secrets
-
Documentation expliquant les modifications apportées et leur impact sur la sécurité
📋 Spécifications techniques
- Application : Application web Python (Flask ou autre framework)
- Dockerfile : Build multi-étapes avec utilisateur non-root
- Secrets : Gestion via fichiers d'environnement ou mécanismes d'orchestration
- Optimisation : L'image finale doit être au moins 50% plus petite que la version originale
🛠️ Indices
- Commencer par analyser le Dockerfile original pour identifier tous les problèmes de sécurité
- Utiliser une image de base Alpine ou slim pour la phase finale
- Regrouper les commandes RUN pour réduire le nombre de couches
- Éliminer tous les outils de développement de l'image finale
- Expliciter les versions de toutes les dépendances dans requirements.txt
🔎 Critères d'acceptation
-
Sécurité :
- Aucun secret ne doit être présent dans l'image finale
- L'application doit s'exécuter sous un utilisateur non-root
- Les permissions des fichiers doivent être correctement configurées
-
Optimisation :
- L'image finale doit être significativement plus petite que l'originale
- Le nombre de couches doit être réduit
- L'image ne doit contenir aucun outil de développement inutile
-
Fonctionnalité :
- L'application doit fonctionner correctement avec les secrets injectés
- Toutes les routes et fonctionnalités doivent être opérationnelles
-
Documentation :
- Les problèmes de sécurité identifiés dans le Dockerfile original
- Les améliorations apportées et leur justification
- Les instructions de build et d'exécution sécurisée