{"id":705,"date":"2026-03-23T22:28:01","date_gmt":"2026-03-23T21:28:01","guid":{"rendered":"https:\/\/secure-pipelines.com\/ci-cd-security\/lab-reproducible-container-builds-pinning-verifying-diffing-2\/"},"modified":"2026-03-25T06:27:35","modified_gmt":"2026-03-25T05:27:35","slug":"lab-reproducible-container-builds-pinning-verifying-diffing-2","status":"publish","type":"post","link":"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/lab-reproducible-container-builds-pinning-verifying-diffing-2\/","title":{"rendered":"Laboratorio: Builds Reproducibles de Contenedores \u2014 Pinning, Verificaci\u00f3n y Diffing de Im\u00e1genes"},"content":{"rendered":"<h2>Descripci\u00f3n general<\/h2>\n<p>Si construyes el mismo Dockerfile dos veces y obtienes im\u00e1genes diferentes, no puedes verificar la integridad del build. Un build no reproducible significa que no tienes forma de confirmar que el artefacto que se ejecuta en producci\u00f3n fue realmente producido a partir del c\u00f3digo fuente que auditaste. Los atacantes pueden explotar esta ambig\u00fcedad para inyectar c\u00f3digo malicioso durante el proceso de build sin ser detectados.<\/p>\n<p>Este laboratorio te gu\u00eda a trav\u00e9s de las fuentes de no reproducibilidad en los builds de contenedores, demuestra t\u00e9cnicas para eliminar cada una y muestra c\u00f3mo verificar la reproducibilidad autom\u00e1ticamente en pipelines de CI\/CD. Al finalizar, tendr\u00e1s un Dockerfile completamente reproducible y un workflow de GitHub Actions que lo demuestra en cada commit.<\/p>\n<h2>Requisitos previos<\/h2>\n<ul>\n<li><strong>Docker con BuildKit<\/strong> \u2014 Docker Desktop 23.0+ tiene BuildKit habilitado por defecto. Verifica con <code>docker buildx version<\/code>.<\/li>\n<li><strong>diffoscope<\/strong> \u2014 Instala con <code>pip install diffoscope<\/code>. Esta herramienta realiza comparaciones profundas y recursivas de archivos y archivos comprimidos.<\/li>\n<li><strong>crane<\/strong> \u2014 Instala desde <a href=\"https:\/\/github.com\/google\/go-containerregistry\/tree\/main\/cmd\/crane\" target=\"_blank\" rel=\"noopener\">go-containerregistry<\/a>. Se utiliza para inspeccionar y manipular im\u00e1genes de contenedores y registros.<\/li>\n<li><strong>Cosign<\/strong> \u2014 Instala desde <a href=\"https:\/\/docs.sigstore.dev\/cosign\/system_config\/installation\/\" target=\"_blank\" rel=\"noopener\">Sigstore<\/a>. Se utiliza para la firma y verificaci\u00f3n de im\u00e1genes de contenedores.<\/li>\n<li><strong>Un repositorio de prueba<\/strong> con un Dockerfile (lo crearemos en el paso de configuraci\u00f3n).<\/li>\n<li><strong>Go 1.22+<\/strong> instalado localmente (opcional, para pruebas locales fuera de Docker).<\/li>\n<\/ul>\n<h2>Configuraci\u00f3n del entorno<\/h2>\n<p>Crea un repositorio de prueba nuevo con una aplicaci\u00f3n Go simple. Esto nos proporciona un proyecto realista y m\u00ednimo con el que trabajar a lo largo del laboratorio.<\/p>\n<h3>Paso 1: Inicializar el proyecto<\/h3>\n<pre><code class=\"language-bash\">mkdir repro-build-lab && cd repro-build-lab\ngit init\ngo mod init github.com\/example\/repro-build-lab<\/code><\/pre>\n<h3>Paso 2: Crear la aplicaci\u00f3n Go<\/h3>\n<p>Crea <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>Paso 3: Crear el Dockerfile intencionalmente no reproducible<\/h3>\n<p>Este Dockerfile contiene todos los errores comunes que conducen a builds no reproducibles:<\/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 && apt-get install -y curl\n\n# Embeds current timestamp into the image\nRUN echo \"Built at $(date)\" > \/build-info\n\nCOPY . .\n\nRUN go build -o \/app .\/cmd\/app\n\nEXPOSE 8080\nCMD [\"\/app\"]<\/code><\/pre>\n<p>Observa los problemas:<\/p>\n<ul>\n<li><code>FROM golang:latest<\/code> \u2014 la imagen base cambia sin previo aviso.<\/li>\n<li><code>apt-get install -y curl<\/code> \u2014 sin pinning de versi\u00f3n, por lo que la versi\u00f3n instalada var\u00eda.<\/li>\n<li><code>echo \"Built at $(date)\"<\/code> \u2014 inyecta una marca de tiempo que es diferente en cada build.<\/li>\n<li>Sin <code>.dockerignore<\/code> \u2014 archivos locales como <code>.git\/<\/code> se filtran en el contexto del build, cambiando los hashes de las capas.<\/li>\n<\/ul>\n<p>Haz commit del proyecto inicial:<\/p>\n<pre><code class=\"language-bash\">git add -A\ngit commit -m \"Initial non-reproducible project\"<\/code><\/pre>\n<h2>Ejercicio 1: Demostrar la no reproducibilidad<\/h2>\n<p>Antes de corregir nada, demostremos que el Dockerfile actual produce im\u00e1genes diferentes en cada build.<\/p>\n<h3>Paso 1: Construir la imagen dos veces<\/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>El flag <code>--no-cache<\/code> obliga a Docker a ejecutar cada capa desde cero, lo cual es esencial para esta comparaci\u00f3n. En un entorno real de CI\/CD, los builds a menudo se ejecutan en runners nuevos sin cach\u00e9.<\/p>\n<h3>Paso 2: Comparar los digests de las im\u00e1genes<\/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>Los digests son <strong>diferentes<\/strong> aunque nada en el c\u00f3digo fuente cambi\u00f3. Esto significa que no puedes verificar que una imagen dada fue producida a partir de un commit espec\u00edfico.<\/p>\n<h3>Paso 3: Usar diffoscope para identificar qu\u00e9 difiere<\/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>Abre <code>diff-report\/index.html<\/code> en un navegador. El informe revela exactamente qu\u00e9 difiere entre los dos builds:<\/p>\n<ul>\n<li><strong>Marcas de tiempo<\/strong> \u2014 el archivo <code>\/build-info<\/code> contiene fechas diferentes.<\/li>\n<li><strong>Metadatos de paquetes apt<\/strong> \u2014 las listas de paquetes y archivos de cach\u00e9 contienen marcas de tiempo y pueden obtener diferentes micro-versiones.<\/li>\n<li><strong>Binario Go<\/strong> \u2014 el binario compilado contiene rutas de build embebidas e IDs de build.<\/li>\n<li><strong>Orden de capas y metadatos<\/strong> \u2014 Docker embebe marcas de tiempo de creaci\u00f3n en los metadatos de las capas.<\/li>\n<\/ul>\n<p>Cada una de estas es una fuente de no reproducibilidad que eliminaremos en los siguientes ejercicios.<\/p>\n<h2>Ejercicio 2: Pinning de la imagen base por digest<\/h2>\n<p>La mayor fuente de variaci\u00f3n es la imagen base. <code>golang:latest<\/code> es un objetivo m\u00f3vil \u2014 puede cambiar entre builds, entre ejecuciones de CI, o incluso entre regiones si un registro es eventualmente consistente.<\/p>\n<h3>Paso 1: Encontrar el digest actual<\/h3>\n<pre><code class=\"language-bash\">crane digest golang:1.22\n# sha256:d0902bacefdde1cf45079803dc16feeb58f3aa9df52052cc00deb2c3e5de367b<\/code><\/pre>\n<h3>Paso 2: Fijar la imagen base<\/h3>\n<p>Actualiza la l\u00ednea <code>FROM<\/code> en el Dockerfile:<\/p>\n<pre><code class=\"language-dockerfile\">FROM golang:1.22@sha256:d0902bacefdde1cf45079803dc16feeb58f3aa9df52052cc00deb2c3e5de367b<\/code><\/pre>\n<p>El formato es <code>image:tag@sha256:digest<\/code>. Docker descargar\u00e1 por digest, ignorando el tag. El tag se mantiene para legibilidad humana.<\/p>\n<h3>Paso 3: Reconstruir y comparar<\/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>Los digests a\u00fan son diferentes \u2014 otras fuentes de no reproducibilidad permanecen. Pero si comparas las capas, la capa de la imagen base ahora es id\u00e9ntica entre builds. Has eliminado la mayor fuente de variaci\u00f3n.<\/p>\n<h3>Por qu\u00e9 esto importa<\/h3>\n<p>Sin pinning por digest, un tag comprometido o secuestrado puede reemplazar silenciosamente tu imagen base con una maliciosa. El pinning por digest es una garant\u00eda criptogr\u00e1fica: obtienes exactamente los bytes que esperas, o el build falla.<\/p>\n<h2>Ejercicio 3: Pinning de versiones de paquetes<\/h2>\n<p>Las versiones flotantes de paquetes introducen no determinismo en la capa de dependencias. Cada vez que se ejecuta <code>apt-get update<\/code>, obtiene el \u00edndice actual del repositorio, que puede listar versiones diferentes de paquetes.<\/p>\n<h3>Opci\u00f3n A: Pinning de versiones de paquetes Debian<\/h3>\n<pre><code class=\"language-dockerfile\">RUN apt-get update && \\\n    apt-get install -y --no-install-recommends \\\n      curl=7.88.1-10+deb12u8 && \\\n    rm -rf \/var\/lib\/apt\/lists\/*<\/code><\/pre>\n<p>Para encontrar la versi\u00f3n actual disponible en tu imagen base:<\/p>\n<pre><code class=\"language-bash\">docker run --rm golang:1.22 apt-cache policy curl<\/code><\/pre>\n<h3>Opci\u00f3n B: Usar Alpine con paquetes fijados<\/h3>\n<p>Los paquetes de Alpine tienen cadenas de versi\u00f3n m\u00e1s simples e im\u00e1genes m\u00e1s peque\u00f1as:<\/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>Opci\u00f3n C: Build multi-stage (preferido)<\/h3>\n<p>El mejor enfoque es evitar instalar paquetes en la imagen final por completo. Usa un build multi-stage donde la etapa de build tiene las herramientas y la etapa de runtime es m\u00ednima:<\/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>Con este enfoque, la imagen de runtime tiene cero llamadas al gestor de paquetes, lo que elimina toda una clase de no reproducibilidad.<\/p>\n<h3>Reconstruir y comparar<\/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>Las capas de paquetes ahora son id\u00e9nticas entre builds. Las diferencias restantes provienen de las marcas de tiempo y del binario Go en s\u00ed.<\/p>\n<h2>Ejercicio 4: Eliminar marcas de tiempo y contenido no determinista<\/h2>\n<p>Las marcas de tiempo son la fuente m\u00e1s obvia de no reproducibilidad. Cualquier comando que capture la hora actual produce un resultado diferente en cada build.<\/p>\n<h3>Paso 1: Eliminar marcas de tiempo expl\u00edcitas<\/h3>\n<p>Elimina la l\u00ednea que escribe la hora del build:<\/p>\n<pre><code class=\"language-dockerfile\"># REMOVE this line:\n# RUN echo \"Built at $(date)\" > \/build-info<\/code><\/pre>\n<p>Si necesitas metadatos del build, p\u00e1salos como una etiqueta con un valor fijo derivado del c\u00f3digo fuente:<\/p>\n<pre><code class=\"language-dockerfile\">ARG BUILD_COMMIT\nLABEL org.opencontainers.image.revision=${BUILD_COMMIT}<\/code><\/pre>\n<h3>Paso 2: Establecer SOURCE_DATE_EPOCH<\/h3>\n<p><code>SOURCE_DATE_EPOCH<\/code> es una <a href=\"https:\/\/reproducible-builds.org\/specs\/source-date-epoch\/\" target=\"_blank\" rel=\"noopener\">variable de entorno estandarizada<\/a> que indica a las herramientas de build que usen una marca de tiempo fija en lugar de la hora actual. Muchas herramientas la respetan, incluyendo <code>tar<\/code>, <code>gzip<\/code>, <code>zip<\/code> y el compilador de Go.<\/p>\n<pre><code class=\"language-dockerfile\">ARG SOURCE_DATE_EPOCH\nENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}<\/code><\/pre>\n<p>Construye con la marca de tiempo del \u00faltimo commit de 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>Esto asegura que los builds del mismo commit siempre usen la misma marca de tiempo, independientemente de cu\u00e1ndo se ejecute realmente el build.<\/p>\n<h3>Paso 3: Usar la salida OCI de BuildKit<\/h3>\n<p>BuildKit puede producir im\u00e1genes en formato OCI con una creaci\u00f3n de capas m\u00e1s determinista:<\/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>El formato de salida OCI evita algunos de los metadatos no deterministas que el formato de imagen predeterminado de Docker incluye.<\/p>\n<h2>Ejercicio 5: Builds reproducibles de Go<\/h2>\n<p>Go embebe varias piezas de informaci\u00f3n no determinista en los binarios compilados por defecto: rutas de archivos locales, un ID de build \u00fanico y s\u00edmbolos de depuraci\u00f3n que referencian el entorno de build.<\/p>\n<h3>Paso 1: Usar flags de build reproducible<\/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>Esto es lo que hace cada flag:<\/p>\n<table>\n<thead>\n<tr>\n<th>Flag<\/th>\n<th>Prop\u00f3sito<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>CGO_ENABLED=0<\/code><\/td>\n<td>Desactiva cgo, produciendo un binario enlazado est\u00e1ticamente. Evita la dependencia de bibliotecas C del sistema que pueden diferir entre builds.<\/td>\n<\/tr>\n<tr>\n<td><code>-trimpath<\/code><\/td>\n<td>Elimina todas las rutas del sistema de archivos local del binario compilado. Sin esto, el binario contiene rutas como <code>\/src\/cmd\/app\/main.go<\/code> del entorno de build.<\/td>\n<\/tr>\n<tr>\n<td><code>-ldflags=\"-s -w\"<\/code><\/td>\n<td>Elimina la tabla de s\u00edmbolos (<code>-s<\/code>) y la informaci\u00f3n de depuraci\u00f3n DWARF (<code>-w<\/code>). Estos contienen datos espec\u00edficos del entorno de build.<\/td>\n<\/tr>\n<tr>\n<td><code>-ldflags=\"-buildid=\"<\/code><\/td>\n<td>Establece el ID de build como vac\u00edo. Go normalmente genera un ID de build \u00fanico que cambia entre builds incluso con c\u00f3digo fuente id\u00e9ntico.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Paso 2: Verificar la reproducibilidad del binario<\/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>Los hashes SHA-256 de <code>app1<\/code> y <code>app2<\/code> deber\u00edan ser id\u00e9nticos. El binario Go ahora es reproducible bit a bit.<\/p>\n<h2>Ejercicio 6: El Dockerfile completamente reproducible<\/h2>\n<p>Ahora combinemos todas las t\u00e9cnicas en un \u00fanico Dockerfile completamente reproducible.<\/p>\n<h3>El Dockerfile completo<\/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 && 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>El .dockerignore completo<\/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>El <code>.dockerignore<\/code> es cr\u00edtico. Sin \u00e9l, el directorio <code>.git\/<\/code> se filtra en el contexto del build. Dado que <code>.git\/<\/code> contiene marcas de tiempo, archivos de bloqueo y otros metadatos cambiantes, hace que cada contexto de build sea \u00fanico incluso cuando el c\u00f3digo fuente es id\u00e9ntico.<\/p>\n<h3>Construir y verificar<\/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>Con todas las t\u00e9cnicas de reproducibilidad aplicadas, los hashes SHA-256 de los dos tarballs OCI deber\u00edan coincidir o ser extremadamente cercanos. Cualquier diferencia restante estar\u00e1 en los metadatos de configuraci\u00f3n de la imagen y puede resolverse con el flag <code>--source-date-epoch<\/code> de BuildKit (disponible en 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>Ejercicio 7: Verificar la reproducibilidad en CI\/CD<\/h2>\n<p>La reproducibilidad solo es valiosa si la verificas continuamente. Un build que es reproducible hoy puede dejar de serlo ma\u00f1ana si alguien agrega una dependencia flotante o una marca de tiempo. La soluci\u00f3n es construir dos veces en cada ejecuci\u00f3n de CI y asegurar que los resultados sean id\u00e9nticos.<\/p>\n<h3>Workflow de GitHub Actions<\/h3>\n<p>Crea <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)\" >> \"$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}')\" >> \"$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}')\" >> \"$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>Este workflow hace lo siguiente en cada push y pull request:<\/p>\n<ol>\n<li>Descarga el c\u00f3digo y configura BuildKit.<\/li>\n<li>Calcula <code>SOURCE_DATE_EPOCH<\/code> a partir de la marca de tiempo del \u00faltimo commit.<\/li>\n<li>Construye la imagen desde cero (primera pasada) y registra el digest.<\/li>\n<li>Construye la imagen desde cero nuevamente (segunda pasada) y registra el digest.<\/li>\n<li>Compara los dos digests. Si difieren, el job falla y ejecuta <code>diffoscope<\/code> para producir un informe detallado de diferencias.<\/li>\n<li>En caso de \u00e9xito en la rama main, la imagen verificada est\u00e1 lista para ser firmada con Cosign.<\/li>\n<\/ol>\n<p>Esta es la garant\u00eda m\u00e1s fuerte que puedes tener: cada ejecuci\u00f3n de CI demuestra que tu build es reproducible. Si un desarrollador introduce no determinismo, el build falla inmediatamente.<\/p>\n<h2>Ejercicio 8: Diffing de im\u00e1genes entre versiones<\/h2>\n<p>Los builds reproducibles tambi\u00e9n te dan la capacidad de hacer diffing entre versiones y verificar que solo los cambios esperados est\u00e1n presentes. Esto es cr\u00edtico para auditar releases: quieres confirmar que un cambio de versi\u00f3n solo modific\u00f3 el binario de la aplicaci\u00f3n, no la imagen base ni los paquetes del sistema.<\/p>\n<h3>Paso 1: Construir la versi\u00f3n 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>Paso 2: Hacer un cambio en el c\u00f3digo<\/h3>\n<p>Edita <code>cmd\/app\/main.go<\/code> para cambiar la cadena de versi\u00f3n:<\/p>\n<pre><code class=\"language-go\">fmt.Fprintf(w, \"Hello from repro-build-lab v2\\n\")<\/code><\/pre>\n<p>Haz commit del cambio:<\/p>\n<pre><code class=\"language-bash\">git add cmd\/app\/main.go\ngit commit -m \"Bump to v2\"<\/code><\/pre>\n<h3>Paso 3: Construir la versi\u00f3n 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>Paso 4: Comparar con diffoscope<\/h3>\n<pre><code class=\"language-bash\">diffoscope image-v1.tar image-v2.tar --html-dir version-diff-report<\/code><\/pre>\n<p>Abre el informe. Deber\u00edas ver que las \u00fanicas diferencias son:<\/p>\n<ul>\n<li>El binario de la aplicaci\u00f3n Go \u2014 porque cambiamos el c\u00f3digo fuente.<\/li>\n<li>El valor de <code>SOURCE_DATE_EPOCH<\/code> \u2014 porque la marca de tiempo del commit cambi\u00f3.<\/li>\n<\/ul>\n<p>Las capas de la imagen base, el runtime distroless y todas las dem\u00e1s capas deber\u00edan ser completamente id\u00e9nticas.<\/p>\n<h3>Paso 5: Comparar capas con 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>Compara los digests de las capas. Ver\u00e1s que todas las capas son id\u00e9nticas excepto la que contiene el binario Go. Esto es exactamente lo que quieres: un cambio de versi\u00f3n solo deber\u00eda modificar la capa de la aplicaci\u00f3n, nada m\u00e1s.<\/p>\n<p>Si ves cambios inesperados en las capas (por ejemplo, la capa de la imagen base difiere), significa que algo rompi\u00f3 la reproducibilidad y necesita investigaci\u00f3n. Esta comparaci\u00f3n capa por capa es una t\u00e9cnica de auditor\u00eda poderosa que solo funciona cuando tus builds son reproducibles.<\/p>\n<h2>Limpieza<\/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>\/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 && docker rm registry 2>\/dev\/null\n\n# Remove the test project (optional)\ncd .. && rm -rf repro-build-lab<\/code><\/pre>\n<h2>Conclusiones clave<\/h2>\n<ul>\n<li><strong>Fija las im\u00e1genes base por digest, no por tag.<\/strong> Los tags son punteros mutables. Los digests son garant\u00edas criptogr\u00e1ficas. Usa <code>crane digest<\/code> para encontrar el digest actual y actual\u00edzalo deliberadamente a trav\u00e9s de un PR, no silenciosamente durante un build.<\/li>\n<li><strong>Fija todas las versiones de paquetes o evita los gestores de paquetes en la imagen de runtime.<\/strong> Los builds multi-stage con im\u00e1genes de runtime distroless o scratch eliminan toda una categor\u00eda de no reproducibilidad.<\/li>\n<li><strong>Elimina todas las fuentes de marcas de tiempo.<\/strong> Usa <code>SOURCE_DATE_EPOCH<\/code> derivado de la marca de tiempo del commit de git. Nunca ejecutes <code>date<\/code>, <code>timestamp<\/code> o comandos similares en un Dockerfile.<\/li>\n<li><strong>Usa flags de compilador reproducibles.<\/strong> Para Go: <code>-trimpath<\/code>, <code>-ldflags=\"-s -w -buildid=\"<\/code> y <code>CGO_ENABLED=0<\/code>. Otros lenguajes tienen opciones similares.<\/li>\n<li><strong>Verifica la reproducibilidad en CI\/CD construyendo dos veces y comparando.<\/strong> Esta es la \u00fanica forma de garantizar que tu build permanezca reproducible a medida que el proyecto evoluciona. Si los digests divergen, haz que el build falle.<\/li>\n<li><strong>Usa diffoscope para auditar cambios entre versiones.<\/strong> Los builds reproducibles permiten diffs significativos de im\u00e1genes. Puedes verificar que un release solo contiene los cambios que pretend\u00edas \u2014 nada m\u00e1s.<\/li>\n<\/ul>\n<h2>Pr\u00f3ximos pasos<\/h2>\n<p>Ahora que puedes producir im\u00e1genes de contenedores reproducibles, explora c\u00f3mo construir una cadena completa de integridad y procedencia alrededor de ellas:<\/p>\n<ul>\n<li><a href=\"\/es\/ci-cd-security\/build-integrity-reproducible-builds-ci-cd\/\">Build Integrity and Reproducible Builds<\/a> \u2014 profundizaci\u00f3n en la teor\u00eda detr\u00e1s de los builds reproducibles, los requisitos de SLSA Build Level y c\u00f3mo la reproducibilidad encaja en un marco m\u00e1s amplio de integridad de builds.<\/li>\n<li><a href=\"\/es\/ci-cd-security\/artifact-provenance-attestations-slsa-in-toto\/\">Artifact Provenance and Attestations: From SLSA to in-toto<\/a> \u2014 aprende c\u00f3mo generar y verificar attestations de procedencia para tus builds reproducibles, creando una cadena auditable desde el c\u00f3digo fuente hasta el despliegue.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Descripci\u00f3n general Si construyes el mismo Dockerfile dos veces y obtienes im\u00e1genes diferentes, no puedes verificar la integridad del build. Un build no reproducible significa que no tienes forma de confirmar que el artefacto que se ejecuta en producci\u00f3n fue realmente producido a partir del c\u00f3digo fuente que auditaste. Los atacantes pueden explotar esta ambig\u00fcedad &#8230; <a title=\"Laboratorio: Builds Reproducibles de Contenedores \u2014 Pinning, Verificaci\u00f3n y Diffing de Im\u00e1genes\" class=\"read-more\" href=\"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/lab-reproducible-container-builds-pinning-verifying-diffing-2\/\" aria-label=\"Leer m\u00e1s sobre Laboratorio: Builds Reproducibles de Contenedores \u2014 Pinning, Verificaci\u00f3n y Diffing de Im\u00e1genes\">Leer m\u00e1s<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[55,59],"tags":[],"post_folder":[],"class_list":["post-705","post","type-post","status-publish","format-standard","hentry","category-ci-cd-security","category-software-supply-chain"],"_links":{"self":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts\/705","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/comments?post=705"}],"version-history":[{"count":0,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts\/705\/revisions"}],"wp:attachment":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/media?parent=705"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/categories?post=705"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/tags?post=705"},{"taxonomy":"post_folder","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/post_folder?post=705"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}