{"id":546,"date":"2026-03-23T22:28:01","date_gmt":"2026-03-23T21:28:01","guid":{"rendered":"https:\/\/secure-pipelines.com\/?p=546"},"modified":"2026-03-24T12:59:27","modified_gmt":"2026-03-24T11:59:27","slug":"lab-reproducible-container-builds-pinning-verifying-diffing","status":"publish","type":"post","link":"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/lab-reproducible-container-builds-pinning-verifying-diffing\/","title":{"rendered":"Lab : Builds Container Reproductibles \u2014 Pinning, V\u00e9rification et Comparaison d&rsquo;Images"},"content":{"rendered":"<h2>Pr\u00e9sentation<\/h2>\n<p>Si vous construisez le m\u00eame Dockerfile deux fois et obtenez des images diff\u00e9rentes, vous ne pouvez pas v\u00e9rifier l&rsquo;int\u00e9grit\u00e9 du build. Un build non reproductible signifie que vous n&rsquo;avez aucun moyen de confirmer que l&rsquo;artefact en production a bien \u00e9t\u00e9 produit \u00e0 partir du code source que vous avez audit\u00e9. Les attaquants peuvent exploiter cette ambigu\u00eft\u00e9 pour injecter du code malveillant durant le processus de build sans \u00eatre d\u00e9tect\u00e9s.<\/p>\n<p>Ce lab vous guide \u00e0 travers les sources de non-reproductibilit\u00e9 dans les builds de containers, d\u00e9montre les techniques pour \u00e9liminer chacune d&rsquo;entre elles, et montre comment v\u00e9rifier automatiquement la reproductibilit\u00e9 dans les pipelines CI\/CD. \u00c0 la fin, vous disposerez d&rsquo;un Dockerfile enti\u00e8rement reproductible et d&rsquo;un workflow GitHub Actions qui le prouve \u00e0 chaque commit.<\/p>\n<h2>Pr\u00e9requis<\/h2>\n<ul>\n<li><strong>Docker avec BuildKit<\/strong> \u2014 Docker Desktop 23.0+ a BuildKit activ\u00e9 par d\u00e9faut. V\u00e9rifiez avec <code>docker buildx version<\/code>.<\/li>\n<li><strong>diffoscope<\/strong> \u2014 Installez avec <code>pip install diffoscope<\/code>. Cet outil effectue une comparaison r\u00e9cursive et approfondie de fichiers et d&rsquo;archives.<\/li>\n<li><strong>crane<\/strong> \u2014 Installez depuis <a href=\"https:\/\/github.com\/google\/go-containerregistry\/tree\/main\/cmd\/crane\" target=\"_blank\" rel=\"noopener\">go-containerregistry<\/a>. Utilis\u00e9 pour inspecter et manipuler les images de containers et les registres.<\/li>\n<li><strong>Cosign<\/strong> \u2014 Installez depuis <a href=\"https:\/\/docs.sigstore.dev\/cosign\/system_config\/installation\/\" target=\"_blank\" rel=\"noopener\">Sigstore<\/a>. Utilis\u00e9 pour la signature et la v\u00e9rification d&rsquo;images de containers.<\/li>\n<li><strong>Un d\u00e9p\u00f4t de test<\/strong> avec un Dockerfile (nous en cr\u00e9erons un lors de l&rsquo;\u00e9tape de configuration).<\/li>\n<li><strong>Go 1.22+<\/strong> install\u00e9 localement (optionnel, pour les tests locaux en dehors de Docker).<\/li>\n<\/ul>\n<h2>Configuration de l&rsquo;environnement<\/h2>\n<p>Cr\u00e9ez un d\u00e9p\u00f4t de test avec une application Go simple. Cela nous donne un projet r\u00e9aliste et minimal pour travailler tout au long du lab.<\/p>\n<h3>\u00c9tape 1 : Initialiser le projet<\/h3>\n<pre><code class=\"language-bash\">mkdir repro-build-lab &amp;&amp; cd repro-build-lab\ngit init\ngo mod init github.com\/example\/repro-build-lab<\/code><\/pre>\n<h3>\u00c9tape 2 : Cr\u00e9er l&rsquo;application Go<\/h3>\n<p>Cr\u00e9ez <code>cmd\/app\/main.go<\/code> :<\/p>\n<pre><code class=\"language-go\">package main\n\nimport (\n\t\"fmt\"\n\t\"net\/http\"\n\t\"os\"\n)\n\nfunc main() {\n\tport := os.Getenv(\"PORT\")\n\tif port == \"\" {\n\t\tport = \"8080\"\n\t}\n\n\thttp.HandleFunc(\"\/\", func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprintf(w, \"Hello from repro-build-lab v1\\n\")\n\t})\n\n\thttp.HandleFunc(\"\/healthz\", func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tfmt.Fprintf(w, \"ok\\n\")\n\t})\n\n\tfmt.Printf(\"Listening on :%s\\n\", port)\n\thttp.ListenAndServe(\":\"+port, nil)\n}<\/code><\/pre>\n<h3>\u00c9tape 3 : Cr\u00e9er le Dockerfile intentionnellement non reproductible<\/h3>\n<p>Ce Dockerfile contient toutes les erreurs courantes qui m\u00e8nent \u00e0 des builds non reproductibles :<\/p>\n<pre><code class=\"language-dockerfile\"># Intentionally non-reproducible Dockerfile\nFROM golang:latest\n\nWORKDIR \/src\n\n# Floating package versions\nRUN apt-get update &amp;&amp; apt-get install -y curl\n\n# Embeds current timestamp into the image\nRUN echo \"Built at $(date)\" &gt; \/build-info\n\nCOPY . .\n\nRUN go build -o \/app .\/cmd\/app\n\nEXPOSE 8080\nCMD [\"\/app\"]<\/code><\/pre>\n<p>Remarquez les probl\u00e8mes :<\/p>\n<ul>\n<li><code>FROM golang:latest<\/code> \u2014 l&rsquo;image de base change sans pr\u00e9venir.<\/li>\n<li><code>apt-get install -y curl<\/code> \u2014 aucun verrouillage de version, donc la version install\u00e9e est flottante.<\/li>\n<li><code>echo \"Built at $(date)\"<\/code> \u2014 injecte un horodatage qui diff\u00e8re \u00e0 chaque build.<\/li>\n<li>Pas de <code>.dockerignore<\/code> \u2014 des fichiers locaux comme <code>.git\/<\/code> s&rsquo;infiltrent dans le contexte de build, modifiant les hachages des couches.<\/li>\n<\/ul>\n<p>Commitez le projet initial :<\/p>\n<pre><code class=\"language-bash\">git add -A\ngit commit -m \"Initial non-reproducible project\"<\/code><\/pre>\n<h2>Exercice 1 : D\u00e9montrer la non-reproductibilit\u00e9<\/h2>\n<p>Avant de corriger quoi que ce soit, prouvons que le Dockerfile actuel produit des images diff\u00e9rentes \u00e0 chaque build.<\/p>\n<h3>\u00c9tape 1 : Construire l&rsquo;image deux fois<\/h3>\n<pre><code class=\"language-bash\"># First build\ndocker build --no-cache -t myapp:build1 .\n\n# Wait a moment so the timestamp differs\nsleep 2\n\n# Second build\ndocker build --no-cache -t myapp:build2 .<\/code><\/pre>\n<p>Le flag <code>--no-cache<\/code> force Docker \u00e0 ex\u00e9cuter chaque couche depuis z\u00e9ro, ce qui est essentiel pour cette comparaison. Dans un environnement CI\/CD r\u00e9el, les builds s&rsquo;ex\u00e9cutent souvent sur des runners neufs sans cache.<\/p>\n<h3>\u00c9tape 2 : Comparer les digests des images<\/h3>\n<pre><code class=\"language-bash\">docker inspect --format='{{.Id}}' myapp:build1\n# sha256:a1b2c3d4e5f6... (example)\n\ndocker inspect --format='{{.Id}}' myapp:build2\n# sha256:f6e5d4c3b2a1... (different!)<\/code><\/pre>\n<p>Les digests sont <strong>diff\u00e9rents<\/strong> m\u00eame si rien dans le code source n&rsquo;a chang\u00e9. Cela signifie que vous ne pouvez pas v\u00e9rifier qu&rsquo;une image donn\u00e9e a \u00e9t\u00e9 produite \u00e0 partir d&rsquo;un commit sp\u00e9cifique.<\/p>\n<h3>\u00c9tape 3 : Utiliser diffoscope pour identifier les diff\u00e9rences<\/h3>\n<pre><code class=\"language-bash\"># Export both images as tarballs\ndocker save myapp:build1 -o build1.tar\ndocker save myapp:build2 -o build2.tar\n\n# Run diffoscope\ndiffoscope build1.tar build2.tar --html-dir diff-report<\/code><\/pre>\n<p>Ouvrez <code>diff-report\/index.html<\/code> dans un navigateur. Le rapport r\u00e9v\u00e8le exactement ce qui diff\u00e8re entre les deux builds :<\/p>\n<ul>\n<li><strong>Horodatages<\/strong> \u2014 le fichier <code>\/build-info<\/code> contient des dates diff\u00e9rentes.<\/li>\n<li><strong>M\u00e9tadonn\u00e9es des paquets apt<\/strong> \u2014 les listes de paquets et les fichiers de cache contiennent des horodatages et peuvent t\u00e9l\u00e9charger des micro-versions diff\u00e9rentes.<\/li>\n<li><strong>Binaire Go<\/strong> \u2014 le binaire compil\u00e9 contient des chemins de build embarqu\u00e9s et des identifiants de build.<\/li>\n<li><strong>Ordre et m\u00e9tadonn\u00e9es des couches<\/strong> \u2014 Docker int\u00e8gre des horodatages de cr\u00e9ation dans les m\u00e9tadonn\u00e9es des couches.<\/li>\n<\/ul>\n<p>Chacun de ces \u00e9l\u00e9ments est une source de non-reproductibilit\u00e9 que nous \u00e9liminerons dans les exercices suivants.<\/p>\n<h2>Exercice 2 : \u00c9pingler l&rsquo;image de base par digest<\/h2>\n<p>La plus grande source de d\u00e9rive est l&rsquo;image de base. <code>golang:latest<\/code> est une cible mouvante \u2014 elle peut changer entre les builds, entre les ex\u00e9cutions CI, ou m\u00eame entre les r\u00e9gions si un registre est \u00e9ventuellement coh\u00e9rent.<\/p>\n<h3>\u00c9tape 1 : Trouver le digest actuel<\/h3>\n<pre><code class=\"language-bash\">crane digest golang:1.22\n# sha256:d0902bacefdde1cf45079803dc16feeb58f3aa9df52052cc00deb2c3e5de367b<\/code><\/pre>\n<h3>\u00c9tape 2 : \u00c9pingler l&rsquo;image de base<\/h3>\n<p>Mettez \u00e0 jour la ligne <code>FROM<\/code> dans le Dockerfile :<\/p>\n<pre><code class=\"language-dockerfile\">FROM golang:1.22@sha256:d0902bacefdde1cf45079803dc16feeb58f3aa9df52052cc00deb2c3e5de367b<\/code><\/pre>\n<p>Le format est <code>image:tag@sha256:digest<\/code>. Docker effectuera le pull par digest, en ignorant le tag. Le tag est conserv\u00e9 pour la lisibilit\u00e9 humaine.<\/p>\n<h3>\u00c9tape 3 : Reconstruire et comparer<\/h3>\n<pre><code class=\"language-bash\">docker build --no-cache -t myapp:pinned1 .\nsleep 2\ndocker build --no-cache -t myapp:pinned2 .\n\ndocker inspect --format='{{.Id}}' myapp:pinned1\ndocker inspect --format='{{.Id}}' myapp:pinned2<\/code><\/pre>\n<p>Les digests sont encore diff\u00e9rents \u2014 d&rsquo;autres sources de non-reproductibilit\u00e9 subsistent. Mais si vous comparez les couches, la couche de l&rsquo;image de base est d\u00e9sormais identique entre les builds. Vous avez \u00e9limin\u00e9 la plus grande source de d\u00e9rive.<\/p>\n<h3>Pourquoi c&rsquo;est important<\/h3>\n<p>Sans \u00e9pinglage par digest, un tag compromis ou d\u00e9tourn\u00e9 peut silencieusement remplacer votre image de base par une image malveillante. L&rsquo;\u00e9pinglage par digest est une garantie cryptographique : vous obtenez exactement les octets attendus, ou le build \u00e9choue.<\/p>\n<h2>Exercice 3 : \u00c9pingler les versions des paquets<\/h2>\n<p>Les versions flottantes des paquets introduisent du non-d\u00e9terminisme dans la couche de d\u00e9pendances. Chaque fois que <code>apt-get update<\/code> s&rsquo;ex\u00e9cute, il r\u00e9cup\u00e8re l&rsquo;index actuel du d\u00e9p\u00f4t, qui peut lister des versions de paquets diff\u00e9rentes.<\/p>\n<h3>Option A : \u00c9pingler les versions des paquets Debian<\/h3>\n<pre><code class=\"language-dockerfile\">RUN apt-get update &amp;&amp; \\\n    apt-get install -y --no-install-recommends \\\n      curl=7.88.1-10+deb12u8 &amp;&amp; \\\n    rm -rf \/var\/lib\/apt\/lists\/*<\/code><\/pre>\n<p>Pour trouver la version actuellement disponible dans votre image de base :<\/p>\n<pre><code class=\"language-bash\">docker run --rm golang:1.22 apt-cache policy curl<\/code><\/pre>\n<h3>Option B : Utiliser Alpine avec des paquets \u00e9pingl\u00e9s<\/h3>\n<p>Les paquets Alpine ont des cha\u00eenes de version plus simples et des images plus petites :<\/p>\n<pre><code class=\"language-dockerfile\">FROM golang:1.22-alpine@sha256:&lt;alpine-digest&gt;\n\nRUN apk add --no-cache curl=8.5.0-r0<\/code><\/pre>\n<h3>Option C : Build multi-\u00e9tapes (recommand\u00e9)<\/h3>\n<p>La meilleure approche est d&rsquo;\u00e9viter compl\u00e8tement l&rsquo;installation de paquets dans l&rsquo;image finale. Utilisez un build multi-\u00e9tapes o\u00f9 l&rsquo;\u00e9tape de build dispose des outils et l&rsquo;\u00e9tape d&rsquo;ex\u00e9cution est minimale :<\/p>\n<pre><code class=\"language-dockerfile\"># Build stage \u2014 tools are only needed here\nFROM golang:1.22@sha256:d0902bacefdde1cf45079803dc16feeb58f3aa9df52052cc00deb2c3e5de367b AS builder\nWORKDIR \/src\nCOPY go.mod go.sum .\/\nRUN go mod download\nCOPY . .\nRUN go build -o \/app .\/cmd\/app\n\n# Runtime stage \u2014 no apt-get, no floating packages\nFROM gcr.io\/distroless\/static-debian12:nonroot\nCOPY --from=builder \/app \/app\nCMD [\"\/app\"]<\/code><\/pre>\n<p>Avec cette approche, l&rsquo;image d&rsquo;ex\u00e9cution n&rsquo;a aucun appel au gestionnaire de paquets, ce qui \u00e9limine toute une classe de non-reproductibilit\u00e9.<\/p>\n<h3>Reconstruire et comparer<\/h3>\n<pre><code class=\"language-bash\">docker build --no-cache -t myapp:pinpkg1 .\nsleep 2\ndocker build --no-cache -t myapp:pinpkg2 .\n\ndocker inspect --format='{{.Id}}' myapp:pinpkg1\ndocker inspect --format='{{.Id}}' myapp:pinpkg2<\/code><\/pre>\n<p>Les couches de paquets sont d\u00e9sormais identiques entre les builds. Les diff\u00e9rences restantes proviennent des horodatages et du binaire Go lui-m\u00eame.<\/p>\n<h2>Exercice 4 : Supprimer les horodatages et le contenu non d\u00e9terministe<\/h2>\n<p>Les horodatages sont la source de non-reproductibilit\u00e9 la plus \u00e9vidente. Toute commande qui capture l&rsquo;heure actuelle produit un r\u00e9sultat diff\u00e9rent \u00e0 chaque build.<\/p>\n<h3>\u00c9tape 1 : Supprimer les horodatages explicites<\/h3>\n<p>Supprimez la ligne qui \u00e9crit l&rsquo;heure de build :<\/p>\n<pre><code class=\"language-dockerfile\"># REMOVE this line:\n# RUN echo \"Built at $(date)\" &gt; \/build-info<\/code><\/pre>\n<p>Si vous avez besoin de m\u00e9tadonn\u00e9es de build, passez-les en tant que label avec une valeur fixe d\u00e9riv\u00e9e de la source :<\/p>\n<pre><code class=\"language-dockerfile\">ARG BUILD_COMMIT\nLABEL org.opencontainers.image.revision=${BUILD_COMMIT}<\/code><\/pre>\n<h3>\u00c9tape 2 : D\u00e9finir SOURCE_DATE_EPOCH<\/h3>\n<p><code>SOURCE_DATE_EPOCH<\/code> est une <a href=\"https:\/\/reproducible-builds.org\/specs\/source-date-epoch\/\" target=\"_blank\" rel=\"noopener\">variable d&rsquo;environnement standardis\u00e9e<\/a> qui indique aux outils de build d&rsquo;utiliser un horodatage fixe au lieu de l&rsquo;heure actuelle. De nombreux outils la respectent, notamment <code>tar<\/code>, <code>gzip<\/code>, <code>zip<\/code> et le compilateur Go.<\/p>\n<pre><code class=\"language-dockerfile\">ARG SOURCE_DATE_EPOCH\nENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}<\/code><\/pre>\n<p>Construisez avec l&rsquo;horodatage du dernier commit git :<\/p>\n<pre><code class=\"language-bash\">docker build \\\n  --build-arg SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) \\\n  --no-cache \\\n  -t myapp:repro .<\/code><\/pre>\n<p>Cela garantit que les builds \u00e0 partir du m\u00eame commit utilisent toujours le m\u00eame horodatage, quelle que soit le moment r\u00e9el du build.<\/p>\n<h3>\u00c9tape 3 : Utiliser la sortie OCI de BuildKit<\/h3>\n<p>BuildKit peut produire des images au format OCI avec une cr\u00e9ation de couches plus d\u00e9terministe :<\/p>\n<pre><code class=\"language-bash\">docker buildx build \\\n  --build-arg SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) \\\n  --output type=oci,dest=myapp.tar \\\n  --no-cache \\\n  .<\/code><\/pre>\n<p>Le format de sortie OCI \u00e9vite certaines m\u00e9tadonn\u00e9es non d\u00e9terministes que le format d&rsquo;image Docker par d\u00e9faut inclut.<\/p>\n<h2>Exercice 5 : Builds Go reproductibles<\/h2>\n<p>Go int\u00e8gre par d\u00e9faut plusieurs informations non d\u00e9terministes dans les binaires compil\u00e9s : les chemins de fichiers locaux, un identifiant de build unique et des symboles de d\u00e9bogage qui r\u00e9f\u00e9rencent l&rsquo;environnement de build.<\/p>\n<h3>\u00c9tape 1 : Utiliser les flags de build reproductible<\/h3>\n<pre><code class=\"language-dockerfile\">RUN CGO_ENABLED=0 go build \\\n    -trimpath \\\n    -ldflags=\"-s -w -buildid=\" \\\n    -o \/app .\/cmd\/app<\/code><\/pre>\n<p>Voici ce que fait chaque flag :<\/p>\n<table>\n<thead>\n<tr>\n<th>Flag<\/th>\n<th>Objectif<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>CGO_ENABLED=0<\/code><\/td>\n<td>D\u00e9sactive cgo, produisant un binaire li\u00e9 statiquement. \u00c9vite la d\u00e9pendance aux biblioth\u00e8ques C syst\u00e8me qui peuvent diff\u00e9rer entre les builds.<\/td>\n<\/tr>\n<tr>\n<td><code>-trimpath<\/code><\/td>\n<td>Supprime tous les chemins du syst\u00e8me de fichiers local du binaire compil\u00e9. Sans cela, le binaire contient des chemins comme <code>\/src\/cmd\/app\/main.go<\/code> de l&rsquo;environnement de build.<\/td>\n<\/tr>\n<tr>\n<td><code>-ldflags=\"-s -w\"<\/code><\/td>\n<td>Supprime la table des symboles (<code>-s<\/code>) et les informations de d\u00e9bogage DWARF (<code>-w<\/code>). Celles-ci contiennent des donn\u00e9es sp\u00e9cifiques \u00e0 l&rsquo;environnement de build.<\/td>\n<\/tr>\n<tr>\n<td><code>-ldflags=\"-buildid=\"<\/code><\/td>\n<td>D\u00e9finit l&rsquo;identifiant de build \u00e0 vide. Go g\u00e9n\u00e8re normalement un identifiant de build unique qui change entre les builds m\u00eame avec un source identique.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>\u00c9tape 2 : V\u00e9rifier la reproductibilit\u00e9 du binaire<\/h3>\n<pre><code class=\"language-bash\"># Build twice\ndocker build --no-cache -t myapp:go1 .\ndocker build --no-cache -t myapp:go2 .\n\n# Extract and hash the binary from each image\ndocker create --name tmp1 myapp:go1\ndocker cp tmp1:\/app .\/app1\ndocker rm tmp1\n\ndocker create --name tmp2 myapp:go2\ndocker cp tmp2:\/app .\/app2\ndocker rm tmp2\n\nsha256sum app1 app2<\/code><\/pre>\n<p>Les hachages SHA-256 de <code>app1<\/code> et <code>app2<\/code> devraient \u00eatre identiques. Le binaire Go est d\u00e9sormais reproductible bit \u00e0 bit.<\/p>\n<h2>Exercice 6 : Le Dockerfile enti\u00e8rement reproductible<\/h2>\n<p>Combinons maintenant toutes les techniques en un seul Dockerfile enti\u00e8rement reproductible.<\/p>\n<h3>Le Dockerfile complet<\/h3>\n<pre><code class=\"language-dockerfile\"># syntax=docker\/dockerfile:1\n\n# ---- Build Stage ----\nFROM golang:1.22@sha256:d0902bacefdde1cf45079803dc16feeb58f3aa9df52052cc00deb2c3e5de367b AS builder\n\nARG SOURCE_DATE_EPOCH\nENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}\n\nWORKDIR \/src\n\n# Cache dependency downloads\nCOPY go.mod go.sum .\/\nRUN go mod download &amp;&amp; go mod verify\n\n# Copy source and build\nCOPY . .\nRUN CGO_ENABLED=0 go build \\\n    -trimpath \\\n    -ldflags=\"-s -w -buildid=\" \\\n    -o \/app .\/cmd\/app\n\n# ---- Runtime Stage ----\nFROM gcr.io\/distroless\/static-debian12:nonroot@sha256:6ec5aa99dc335b19f6c2bcb8e09cf92404e56f0db4e2f58cf92c4536e1548415\n\nARG SOURCE_DATE_EPOCH\nENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}\n\nCOPY --from=builder \/app \/app\n\nUSER nonroot:nonroot\nEXPOSE 8080\nENTRYPOINT [\"\/app\"]<\/code><\/pre>\n<h3>Le fichier .dockerignore complet<\/h3>\n<pre><code class=\"language-text\">.git\n.github\n.gitignore\n*.md\nREADME*\nLICENSE\ndocker-compose*.yml\nMakefile\n.env\n.env.*\n*.tar\n*.log\ntmp\/\nbuild\/\ndiff-report\/\n<\/code><\/pre>\n<p>Le <code>.dockerignore<\/code> est crucial. Sans lui, le r\u00e9pertoire <code>.git\/<\/code> s&rsquo;infiltre dans le contexte de build. Comme <code>.git\/<\/code> contient des horodatages, des fichiers de verrouillage et d&rsquo;autres m\u00e9tadonn\u00e9es changeantes, il rend chaque contexte de build unique m\u00eame lorsque le source est identique.<\/p>\n<h3>Construire et v\u00e9rifier<\/h3>\n<pre><code class=\"language-bash\">SOURCE_EPOCH=$(git log -1 --format=%ct)\n\n# Build twice\ndocker buildx build \\\n  --build-arg SOURCE_DATE_EPOCH=${SOURCE_EPOCH} \\\n  --output type=oci,dest=build1.tar \\\n  --no-cache .\n\ndocker buildx build \\\n  --build-arg SOURCE_DATE_EPOCH=${SOURCE_EPOCH} \\\n  --output type=oci,dest=build2.tar \\\n  --no-cache .\n\n# Compare\nsha256sum build1.tar build2.tar<\/code><\/pre>\n<p>Avec toutes les techniques de reproductibilit\u00e9 appliqu\u00e9es, les hachages SHA-256 des deux archives OCI devraient correspondre ou \u00eatre extr\u00eamement proches. Toute diff\u00e9rence restante se trouvera dans les m\u00e9tadonn\u00e9es de configuration de l&rsquo;image et peut \u00eatre r\u00e9solue avec le flag <code>--source-date-epoch<\/code> de BuildKit (disponible dans BuildKit 0.13+) :<\/p>\n<pre><code class=\"language-bash\">docker buildx build \\\n  --build-arg SOURCE_DATE_EPOCH=${SOURCE_EPOCH} \\\n  --source-date-epoch ${SOURCE_EPOCH} \\\n  --output type=oci,dest=build-final.tar \\\n  --no-cache .<\/code><\/pre>\n<h2>Exercice 7 : V\u00e9rifier la reproductibilit\u00e9 en CI\/CD<\/h2>\n<p>La reproductibilit\u00e9 n&rsquo;a de valeur que si vous la v\u00e9rifiez en continu. Un build reproductible aujourd&rsquo;hui peut devenir non reproductible demain si quelqu&rsquo;un ajoute une d\u00e9pendance flottante ou un horodatage. La solution est de construire deux fois \u00e0 chaque ex\u00e9cution CI et d&rsquo;affirmer que les r\u00e9sultats sont identiques.<\/p>\n<h3>Workflow GitHub Actions<\/h3>\n<p>Cr\u00e9ez <code>.github\/workflows\/reproducible-build.yml<\/code> :<\/p>\n<pre><code class=\"language-yaml\">name: Verify Reproducible Build\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  verify-reproducibility:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions\/checkout@v4\n\n      - name: Set up Docker Buildx\n        uses: docker\/setup-buildx-action@v3\n\n      - name: Compute SOURCE_DATE_EPOCH\n        id: epoch\n        run: echo \"value=$(git log -1 --format=%ct)\" &gt;&gt; \"$GITHUB_OUTPUT\"\n\n      - name: Build image (first pass)\n        run: |\n          docker buildx build \\\n            --build-arg SOURCE_DATE_EPOCH=${{ steps.epoch.outputs.value }} \\\n            --output type=oci,dest=build-pass1.tar \\\n            --no-cache \\\n            .\n\n      - name: Record first digest\n        id: digest1\n        run: echo \"sha=$(sha256sum build-pass1.tar | awk '{print $1}')\" &gt;&gt; \"$GITHUB_OUTPUT\"\n\n      - name: Build image (second pass)\n        run: |\n          docker buildx build \\\n            --build-arg SOURCE_DATE_EPOCH=${{ steps.epoch.outputs.value }} \\\n            --output type=oci,dest=build-pass2.tar \\\n            --no-cache \\\n            .\n\n      - name: Record second digest\n        id: digest2\n        run: echo \"sha=$(sha256sum build-pass2.tar | awk '{print $1}')\" &gt;&gt; \"$GITHUB_OUTPUT\"\n\n      - name: Compare digests\n        run: |\n          echo \"Build 1: ${{ steps.digest1.outputs.sha }}\"\n          echo \"Build 2: ${{ steps.digest2.outputs.sha }}\"\n          if [ \"${{ steps.digest1.outputs.sha }}\" != \"${{ steps.digest2.outputs.sha }}\" ]; then\n            echo \"::error::Builds are NOT reproducible! Digests differ.\"\n            echo \"Running diffoscope to identify differences...\"\n            pip install diffoscope\n            diffoscope build-pass1.tar build-pass2.tar --text diff-output.txt || true\n            cat diff-output.txt\n            exit 1\n          fi\n          echo \"Builds are reproducible. Digests match.\"\n\n      - name: Upload diff report on failure\n        if: failure()\n        uses: actions\/upload-artifact@v4\n        with:\n          name: reproducibility-diff\n          path: diff-output.txt\n\n      - name: Sign the verified image\n        if: github.ref == 'refs\/heads\/main'\n        env:\n          COSIGN_EXPERIMENTAL: \"true\"\n        run: |\n          # Load the OCI image into Docker\n          docker load -i build-pass1.tar\n          # In production, push to a registry and sign with Cosign:\n          # cosign sign --yes $REGISTRY\/$IMAGE@$DIGEST\n          echo \"Image verified as reproducible and ready for signing.\"\n<\/code><\/pre>\n<p>Ce workflow effectue les actions suivantes \u00e0 chaque push et pull request :<\/p>\n<ol>\n<li>R\u00e9cup\u00e8re le code et configure BuildKit.<\/li>\n<li>Calcule <code>SOURCE_DATE_EPOCH<\/code> \u00e0 partir de l&rsquo;horodatage du dernier commit.<\/li>\n<li>Construit l&rsquo;image depuis z\u00e9ro (premi\u00e8re passe) et enregistre le digest.<\/li>\n<li>Construit l&rsquo;image depuis z\u00e9ro \u00e0 nouveau (deuxi\u00e8me passe) et enregistre le digest.<\/li>\n<li>Compare les deux digests. S&rsquo;ils diff\u00e8rent, le job \u00e9choue et ex\u00e9cute <code>diffoscope<\/code> pour produire un rapport de diff d\u00e9taill\u00e9.<\/li>\n<li>En cas de succ\u00e8s sur la branche main, l&rsquo;image v\u00e9rifi\u00e9e est pr\u00eate pour la signature avec Cosign.<\/li>\n<\/ol>\n<p>C&rsquo;est la garantie la plus forte que vous puissiez avoir : chaque ex\u00e9cution CI prouve que votre build est reproductible. Si un d\u00e9veloppeur introduit du non-d\u00e9terminisme, le build \u00e9choue imm\u00e9diatement.<\/p>\n<h2>Exercice 8 : Comparer les images entre versions<\/h2>\n<p>Les builds reproductibles vous donnent \u00e9galement la capacit\u00e9 de comparer les versions entre elles et de v\u00e9rifier que seuls les changements attendus sont pr\u00e9sents. C&rsquo;est essentiel pour l&rsquo;audit des releases : vous voulez confirmer qu&rsquo;un changement de version n&rsquo;a modifi\u00e9 que le binaire de l&rsquo;application, pas l&rsquo;image de base ou les paquets syst\u00e8me.<\/p>\n<h3>\u00c9tape 1 : Construire la version 1<\/h3>\n<pre><code class=\"language-bash\">SOURCE_EPOCH=$(git log -1 --format=%ct)\n\ndocker buildx build \\\n  --build-arg SOURCE_DATE_EPOCH=${SOURCE_EPOCH} \\\n  --output type=oci,dest=image-v1.tar \\\n  --no-cache .<\/code><\/pre>\n<h3>\u00c9tape 2 : Effectuer une modification du code<\/h3>\n<p>Modifiez <code>cmd\/app\/main.go<\/code> pour changer la cha\u00eene de version :<\/p>\n<pre><code class=\"language-go\">fmt.Fprintf(w, \"Hello from repro-build-lab v2\\n\")<\/code><\/pre>\n<p>Commitez le changement :<\/p>\n<pre><code class=\"language-bash\">git add cmd\/app\/main.go\ngit commit -m \"Bump to v2\"<\/code><\/pre>\n<h3>\u00c9tape 3 : Construire la version 2<\/h3>\n<pre><code class=\"language-bash\">SOURCE_EPOCH=$(git log -1 --format=%ct)\n\ndocker buildx build \\\n  --build-arg SOURCE_DATE_EPOCH=${SOURCE_EPOCH} \\\n  --output type=oci,dest=image-v2.tar \\\n  --no-cache .<\/code><\/pre>\n<h3>\u00c9tape 4 : Comparer avec diffoscope<\/h3>\n<pre><code class=\"language-bash\">diffoscope image-v1.tar image-v2.tar --html-dir version-diff-report<\/code><\/pre>\n<p>Ouvrez le rapport. Vous devriez voir que les seules diff\u00e9rences sont :<\/p>\n<ul>\n<li>Le binaire de l&rsquo;application Go \u2014 car nous avons modifi\u00e9 le code source.<\/li>\n<li>La valeur de <code>SOURCE_DATE_EPOCH<\/code> \u2014 car l&rsquo;horodatage du commit a chang\u00e9.<\/li>\n<\/ul>\n<p>Les couches de l&rsquo;image de base, le runtime distroless et toutes les autres couches devraient \u00eatre compl\u00e8tement identiques.<\/p>\n<h3>\u00c9tape 5 : Comparer les couches avec crane<\/h3>\n<pre><code class=\"language-bash\"># Load images and push to a local registry for crane inspection\ndocker run -d -p 5000:5000 --name registry registry:2\n\n# Load and push v1\ndocker load -i image-v1.tar\ndocker tag myapp:latest localhost:5000\/myapp:v1\ndocker push localhost:5000\/myapp:v1\n\n# Load and push v2\ndocker load -i image-v2.tar\ndocker tag myapp:latest localhost:5000\/myapp:v2\ndocker push localhost:5000\/myapp:v2\n\n# List layers for each version\ncrane manifest localhost:5000\/myapp:v1 | jq '.layers[].digest'\ncrane manifest localhost:5000\/myapp:v2 | jq '.layers[].digest'<\/code><\/pre>\n<p>Comparez les digests des couches. Vous verrez que toutes les couches sont identiques sauf celle contenant le binaire Go. C&rsquo;est exactement ce que vous voulez : un changement de version ne devrait modifier que la couche applicative, rien d&rsquo;autre.<\/p>\n<p>Si vous constatez des changements de couches inattendus (par exemple, la couche de l&rsquo;image de base diff\u00e8re), cela signifie que quelque chose a cass\u00e9 la reproductibilit\u00e9 et n\u00e9cessite une investigation. Cette comparaison couche par couche est une technique d&rsquo;audit puissante qui ne fonctionne que lorsque vos builds sont reproductibles.<\/p>\n<h2>Nettoyage<\/h2>\n<pre><code class=\"language-bash\"># Remove test images\ndocker rmi myapp:build1 myapp:build2 myapp:pinned1 myapp:pinned2 \\\n  myapp:pinpkg1 myapp:pinpkg2 myapp:go1 myapp:go2 myapp:repro 2&gt;\/dev\/null\n\n# Remove OCI tarballs\nrm -f build1.tar build2.tar build-pass1.tar build-pass2.tar \\\n  image-v1.tar image-v2.tar myapp.tar\n\n# Remove extracted binaries\nrm -f app1 app2\n\n# Remove diff reports\nrm -rf diff-report version-diff-report\n\n# Stop and remove the local registry\ndocker stop registry &amp;&amp; docker rm registry 2&gt;\/dev\/null\n\n# Remove the test project (optional)\ncd .. &amp;&amp; rm -rf repro-build-lab<\/code><\/pre>\n<h2>Points cl\u00e9s \u00e0 retenir<\/h2>\n<ul>\n<li><strong>\u00c9pinglez les images de base par digest, pas par tag.<\/strong> Les tags sont des pointeurs mutables. Les digests sont des garanties cryptographiques. Utilisez <code>crane digest<\/code> pour trouver le digest actuel et mettez-le \u00e0 jour d\u00e9lib\u00e9r\u00e9ment via une PR, pas silencieusement pendant un build.<\/li>\n<li><strong>\u00c9pinglez toutes les versions de paquets ou \u00e9vitez les gestionnaires de paquets dans l&rsquo;image d&rsquo;ex\u00e9cution.<\/strong> Les builds multi-\u00e9tapes avec des images d&rsquo;ex\u00e9cution distroless ou scratch \u00e9liminent toute une cat\u00e9gorie de non-reproductibilit\u00e9.<\/li>\n<li><strong>\u00c9liminez toutes les sources d&rsquo;horodatages.<\/strong> Utilisez <code>SOURCE_DATE_EPOCH<\/code> d\u00e9riv\u00e9 de l&rsquo;horodatage du commit git. N&rsquo;ex\u00e9cutez jamais <code>date<\/code>, <code>timestamp<\/code> ou des commandes similaires dans un Dockerfile.<\/li>\n<li><strong>Utilisez des flags de compilateur reproductibles.<\/strong> Pour Go : <code>-trimpath<\/code>, <code>-ldflags=\"-s -w -buildid=\"<\/code> et <code>CGO_ENABLED=0<\/code>. D&rsquo;autres langages disposent d&rsquo;options similaires.<\/li>\n<li><strong>V\u00e9rifiez la reproductibilit\u00e9 en CI\/CD en construisant deux fois et en comparant.<\/strong> C&rsquo;est le seul moyen de garantir que votre build reste reproductible au fur et \u00e0 mesure de l&rsquo;\u00e9volution du projet. Si les digests divergent, faites \u00e9chouer le build.<\/li>\n<li><strong>Utilisez diffoscope pour auditer les changements entre versions.<\/strong> Les builds reproductibles permettent des diffs d&rsquo;images significatifs. Vous pouvez v\u00e9rifier qu&rsquo;une release ne contient que les changements pr\u00e9vus \u2014 rien de plus.<\/li>\n<\/ul>\n<h2>Prochaines \u00e9tapes<\/h2>\n<p>Maintenant que vous pouvez produire des images de containers reproductibles, explorez comment construire une cha\u00eene compl\u00e8te d&rsquo;int\u00e9grit\u00e9 et de provenance autour d&rsquo;elles :<\/p>\n<ul>\n<li><a href=\"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/build-integrity-reproducible-builds-ci-cd\/\">Int\u00e9grit\u00e9 des builds et builds reproductibles<\/a> \u2014 approfondissement de la th\u00e9orie derri\u00e8re les builds reproductibles, les exigences de niveau de build SLSA, et comment la reproductibilit\u00e9 s&rsquo;int\u00e8gre dans un cadre plus large d&rsquo;int\u00e9grit\u00e9 des builds.<\/li>\n<li><a href=\"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/artifact-provenance-attestations-slsa-in-toto-2\/\">Provenance des artefacts et attestations : de SLSA \u00e0 in-toto<\/a> \u2014 apprenez comment g\u00e9n\u00e9rer et v\u00e9rifier les attestations de provenance pour vos builds reproductibles, cr\u00e9ant une cha\u00eene auditable de la source au d\u00e9ploiement.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Pr\u00e9sentation Si vous construisez le m\u00eame Dockerfile deux fois et obtenez des images diff\u00e9rentes, vous ne pouvez pas v\u00e9rifier l&rsquo;int\u00e9grit\u00e9 du build. Un build non reproductible signifie que vous n&rsquo;avez aucun moyen de confirmer que l&rsquo;artefact en production a bien \u00e9t\u00e9 produit \u00e0 partir du code source que vous avez audit\u00e9. Les attaquants peuvent exploiter &#8230; <a title=\"Lab : Builds Container Reproductibles \u2014 Pinning, V\u00e9rification et Comparaison d&rsquo;Images\" class=\"read-more\" href=\"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/lab-reproducible-container-builds-pinning-verifying-diffing\/\" aria-label=\"En savoir plus sur Lab : Builds Container Reproductibles \u2014 Pinning, V\u00e9rification et Comparaison d&rsquo;Images\">Lire la suite<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[49,50],"tags":[],"post_folder":[],"class_list":["post-546","post","type-post","status-publish","format-standard","hentry","category-ci-cd-security","category-software-supply-chain"],"_links":{"self":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts\/546","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/comments?post=546"}],"version-history":[{"count":2,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts\/546\/revisions"}],"predecessor-version":[{"id":586,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts\/546\/revisions\/586"}],"wp:attachment":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/media?parent=546"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/categories?post=546"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/tags?post=546"},{"taxonomy":"post_folder","embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/post_folder?post=546"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}