DevSecOps 07 · Sécurité des conteneurs
Sommaire
- Objectifs
- Pourquoi ça te concerne en embarqué
- Théorie utile
- Un conteneur n'est pas une VM
- Les couches sont publiques et permanentes
- Rootless (podman) — ton avantage par défaut
- Lab 1 — image durcie, multi-stage
- Lab 2 — Trivy en gate
- Lab 3 — signature et vérification (Cosign / Sigstore)
- Lab 4 — conteneur de build firmware reproductible
- Critères de validation
- Pièges classiques
- Pour aller plus loin
Durée : 6–8 h. Objectif : construire une image minimale, non-root, scannée et signée — et comprendre pourquoi ça compte aussi quand on fait de l'embarqué.
Objectifs
- Écrire un Containerfile multi-stage durci.
- Scanner une image (Trivy) et refuser la publication sur critère.
- Signer une image avec Cosign et vérifier la signature au déploiement.
- Appliquer ces mêmes principes à un environnement de build firmware reproductible.
Pourquoi ça te concerne en embarqué
- Ta chaîne de compilation croisée vit dans un conteneur (c'est ce qui rend le build reproductible et identique entre ton poste et la CI).
- Les passerelles IoT modernes font tourner des conteneurs (Balena, Azure IoT Edge, systemd-
nspawn, Docker sur Yocto via
meta-virtualization). - Les techniques — surface minimale, non-root, capabilities, FS en lecture seule, signature — sont exactement celles du durcissement d'un Linux embarqué. Tu apprends les deux d'un coup.
Théorie utile
Un conteneur n'est pas une VM
Namespaces (isolation de vue) + cgroups (limitation de ressources) + capabilities + seccomp + LSM (SELinux sur Fedora). Le noyau est partagé : une faille noyau = évasion possible.
Les couches sont publiques et permanentes
podman history image:tag
# un secret ajouté puis supprimé dans un RUN ultérieur reste dans la couche précédente
Rootless (podman) — ton avantage par défaut
Podman en rootless fait tourner le conteneur sous ton uid via user_namespaces : une évasion ne
donne pas root sur l'hôte. Docker avec daemon root est un point d'escalade bien connu
(docker.sock accessible = root sur l'hôte).
Lab 1 — image durcie, multi-stage
api/Containerfile :
# ---------- étape 1 : build ----------
FROM docker.io/library/python:3.11-slim@sha256:<digest> AS build
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --require-hashes -r requirements.txt --target /deps
COPY src/ /deps/src/
# ---------- étape 2 : runtime ----------
FROM gcr.io/distroless/python3-debian12:nonroot
COPY --from=build /deps /app
WORKDIR /app
ENV PYTHONPATH=/app PYTHONDONTWRITEBYTECODE=1
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["python3", "src/main.py"]
Points à savoir justifier :
| Choix | Raison |
|---|---|
| image de base épinglée par digest | un tag est mutable, un digest non |
| multi-stage | compilateurs et sources absents de l'image finale |
| distroless | pas de shell : un RCE ne donne pas de /bin/sh |
USER nonroot |
le processus n'est pas uid 0 dans le conteneur |
--require-hashes |
protège du typosquatting et d'un paquet remplacé en amont |
Exécution durcie :
podman run --rm \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=16m \
--cap-drop=ALL \
--security-opt no-new-privileges \
--security-opt seccomp=/usr/share/containers/seccomp.json \
--memory=256m --pids-limit=100 \
--network=none \
mon-api:1.0
Lab 2 — Trivy en gate
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 mon-api:1.0
trivy image --scanners vuln,secret,misconfig -f json -o trivy.json mon-api:1.0
trivy fs --scanners secret,misconfig .
trivy config api/Containerfile # bonnes pratiques Dockerfile
--ignore-unfixed est un choix assumé : on ne bloque pas sur ce qui n'a pas de correctif
disponible, mais ces CVE doivent apparaître dans un rapport suivi.
En CI :
image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # pour la signature keyless
steps:
- uses: actions/checkout@v4
- name: Build
run: podman build -t ghcr.io/${{ github.repository }}:${{ github.sha }} api/
- name: Scan
uses: aquasecurity/trivy-action@v0
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
severity: HIGH,CRITICAL
ignore-unfixed: true
exit-code: '1'
- name: Publier
run: podman push ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Signer (keyless, OIDC)
run: |
cosign sign --yes ghcr.io/${{ github.repository }}:${{ github.sha }}
Lab 3 — signature et vérification (Cosign / Sigstore)
# signature avec clé (mode classique)
cosign generate-key-pair
cosign sign --key cosign.key ghcr.io/moi/mon-api:1.0
cosign verify --key cosign.pub ghcr.io/moi/mon-api:1.0
# signature keyless : identité OIDC + certificat éphémère + journal public Rekor
cosign sign ghcr.io/moi/mon-api:1.0
cosign verify ghcr.io/moi/mon-api:1.0 \
--certificate-identity-regexp 'https://github.com/yassirmouyiwa/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Attacher un SBOM et une attestation de provenance :
syft mon-api:1.0 -o cyclonedx-json=sbom.json
cosign attest --predicate sbom.json --type cyclonedx ghcr.io/moi/mon-api:1.0
cosign verify-attestation --type cyclonedx ghcr.io/moi/mon-api:1.0
Une signature sans vérification au déploiement ne sert à rien. Le point de contrôle est côté consommateur : admission controller Kubernetes (Kyverno, Connaisseur), ou, côté objet, vérification de la signature avant application d'une mise à jour OTA (module 10).
Lab 4 — conteneur de build firmware reproductible
FROM docker.io/library/debian:bookworm-slim@sha256:<digest>
ARG TOOLCHAIN_VERSION=13.2.rel1
ARG TOOLCHAIN_SHA256=<sha256 attendu>
RUN apt-get update && apt-get install -y --no-install-recommends \
make cmake ninja-build python3 git ca-certificates curl xz-utils \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSLo /tmp/tc.tar.xz \
"https://developer.arm.com/-/media/Files/downloads/gnu/${TOOLCHAIN_VERSION}/binrel/arm-gnu-toolchain-${TOOLCHAIN_VERSION}-x86_64-arm-none-eabi.tar.xz" \
&& echo "${TOOLCHAIN_SHA256} /tmp/tc.tar.xz" | sha256sum -c - \
&& tar -xJf /tmp/tc.tar.xz -C /opt && rm /tmp/tc.tar.xz
ENV PATH="/opt/arm-gnu-toolchain-${TOOLCHAIN_VERSION}-x86_64-arm-none-eabi/bin:${PATH}"
ENV SOURCE_DATE_EPOCH=1700000000
RUN useradd -m build
USER build
WORKDIR /src
La vérification sha256sum -c sur la toolchain téléchargée est le contrôle qui manque presque
partout. Sans elle, ta chaîne de compilation entière repose sur la confiance dans un CDN.
podman build -t firmware-build:1.0 -f Containerfile.build .
podman run --rm -v "$PWD:/src:z" -u build firmware-build:1.0 make
Critères de validation
- Ton image finale fait < 100 Mo et ne contient pas de shell (
podman run --rm -it img shéchoue) - Le processus tourne en non-root (
podman top <ctr> user) - Trivy ne remonte plus aucun HIGH/CRITICAL corrigeable
- L'image est signée et tu sais la vérifier depuis une autre machine
- Le conteneur de build produit deux fois de suite le même binaire (
sha256sum)
Pièges classiques
FROM ubuntu:latest— non reproductible, gros, souvent vulnérable.ADDau lieu deCOPY—ADDdéballe des archives et télécharge des URL.- Scanner l'image mais jamais l'image de base : 90 % des CVE viennent de la base ; mets à jour la base en premier, la plupart des alertes tombent.
--privilegedpour « faire marcher » l'accès USB au programmateur : préfère--device=/dev/ttyUSB0et un groupe dédié.:zoublié sur les volumes en Fedora : SELinux bloque, et on désactive SELinux au lieu d'étiqueter correctement. Ne désactive pas SELinux.
Pour aller plus loin
- CIS Docker Benchmark,
docker-bench-security - Kubernetes : Pod Security Standards, NetworkPolicy, Kyverno
dive(analyse des couches),skopeo(copie/inspection sans daemon)