{"id":661,"date":"2026-03-04T19:20:46","date_gmt":"2026-03-04T18:20:46","guid":{"rendered":"https:\/\/secure-pipelines.com\/?p=661"},"modified":"2026-03-24T18:08:00","modified_gmt":"2026-03-24T17:08:00","slug":"lab-signing-verifying-container-images-cosign-github-actions","status":"publish","type":"post","link":"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/lab-signing-verifying-container-images-cosign-github-actions\/","title":{"rendered":"Lab: Firma y Verificaci\u00f3n de Im\u00e1genes Container con Cosign en GitHub Actions"},"content":{"rendered":"<h2>Descripci\u00f3n General<\/h2>\n<p>Cada imagen de contenedor que produce tu pipeline de CI\/CD debe estar firmada criptogr\u00e1ficamente antes de llegar a cualquier entorno. Las im\u00e1genes sin firmar son un punto ciego \u2014 no tienes prueba de que provengan de tu pipeline, no tienes garant\u00eda de que no hayan sido manipuladas en tr\u00e1nsito, y no tienes ning\u00fan mecanismo de pol\u00edtica para bloquear despliegues no autorizados.<\/p>\n<p>En este laboratorio pr\u00e1ctico:<\/p>\n<ul>\n<li>Firmar\u00e1s una imagen de contenedor localmente usando un par de claves de Cosign.<\/li>\n<li>Configurar\u00e1s la firma keyless en GitHub Actions usando la infraestructura Fulcio y Rekor de Sigstore.<\/li>\n<li>Verificar\u00e1s firmas localmente con comprobaciones de certificados basadas en identidad.<\/li>\n<li>Aplicar\u00e1s la verificaci\u00f3n de firmas en tiempo de admisi\u00f3n en Kubernetes usando Kyverno.<\/li>\n<li>Adjuntar\u00e1s y verificar\u00e1s una attestation de SBOM con Cosign y Syft.<\/li>\n<\/ul>\n<p>Al final tendr\u00e1s un workflow completo de GitHub Actions que construye, publica, firma y certifica cada imagen \u2014 y una pol\u00edtica de Kubernetes que rechaza cualquier imagen sin firmar.<\/p>\n<h2>Requisitos Previos<\/h2>\n<p>Antes de comenzar, aseg\u00farate de tener lo siguiente listo:<\/p>\n<ul>\n<li><strong>Cuenta de GitHub<\/strong> con permisos para crear repositorios y habilitar GitHub Actions.<\/li>\n<li><strong>Cuenta de registro de contenedores<\/strong> \u2014 este laboratorio usa GitHub Container Registry (GHCR), pero Docker Hub tambi\u00e9n funciona.<\/li>\n<li><strong>Docker<\/strong> instalado y ejecut\u00e1ndose localmente.<\/li>\n<li><strong>Cosign CLI<\/strong> instalado localmente:<\/li>\n<\/ul>\n<pre><code># macOS (Homebrew)\nbrew install cosign\n\n# Or install from source with Go\ngo install github.com\/sigstore\/cosign\/v2\/cmd\/cosign@latest\n\n# Verify installation\ncosign version<\/code><\/pre>\n<ul>\n<li><strong>kubectl<\/strong> y <strong>Helm<\/strong> instalados (para el ejercicio de Kyverno).<\/li>\n<li><strong>Syft<\/strong> instalado (para el ejercicio de SBOM):<\/li>\n<\/ul>\n<pre><code>brew install syft<\/code><\/pre>\n<p>Tambi\u00e9n necesitar\u00e1s una aplicaci\u00f3n simple para containerizar. Aqu\u00ed tienes una aplicaci\u00f3n Go m\u00ednima y su Dockerfile que usaremos a lo largo del laboratorio.<\/p>\n<p><strong>main.go<\/strong><\/p>\n<pre><code>package main\n\nimport (\n\t\"fmt\"\n\t\"net\/http\"\n)\n\nfunc main() {\n\thttp.HandleFunc(\"\/\", func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprintf(w, \"Hello from a signed container!\")\n\t})\n\thttp.ListenAndServe(\":8080\", nil)\n}<\/code><\/pre>\n<p><strong>Dockerfile<\/strong><\/p>\n<pre><code>FROM golang:1.22-alpine AS builder\nWORKDIR \/app\nCOPY main.go .\nRUN go build -o server main.go\n\nFROM alpine:3.19\nCOPY --from=builder \/app\/server \/server\nEXPOSE 8080\nENTRYPOINT [\"\/server\"]<\/code><\/pre>\n<h2>Configuraci\u00f3n del Entorno<\/h2>\n<p>Comienza creando un repositorio de prueba y publicando el c\u00f3digo de la aplicaci\u00f3n.<\/p>\n<h3>Paso 1 \u2014 Crear el repositorio<\/h3>\n<pre><code># Create a new directory and initialize a Git repo\nmkdir cosign-lab && cd cosign-lab\ngit init\n\n# Create the Go application and Dockerfile from the prerequisites above\n# Then push to GitHub\ngit add .\ngit commit -m \"Initial commit: simple Go app\"\ngh repo create cosign-lab --public --source=. --push<\/code><\/pre>\n<h3>Paso 2 \u2014 Crear el workflow sin firma<\/h3>\n<p>Antes de agregar la firma, crea un workflow base que solo construya y publique la imagen. Esto te da algo con qu\u00e9 comparar m\u00e1s adelante.<\/p>\n<p>Crea <code>.github\/workflows\/build.yml<\/code>:<\/p>\n<pre><code>name: Build and Push (Unsigned)\n\non:\n  push:\n    tags:\n      - 'v*'\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Checkout code\n        uses: actions\/checkout@v4\n\n      - name: Log in to GHCR\n        uses: docker\/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata\n        id: meta\n        uses: docker\/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}\/${{ env.IMAGE_NAME }}\n\n      - name: Build and push\n        uses: docker\/build-push-action@v5\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}<\/code><\/pre>\n<p>Haz commit y push de este workflow. Crea un tag para activarlo:<\/p>\n<pre><code>git add .\ngit commit -m \"Add unsigned build workflow\"\ngit push origin main\ngit tag v0.1.0\ngit push origin v0.1.0<\/code><\/pre>\n<p>Una vez que el workflow se complete, tu imagen estar\u00e1 en GHCR \u2014 pero no lleva ninguna firma criptogr\u00e1fica. Cualquier persona con acceso de escritura al registro podr\u00eda reemplazarla, y nada aguas abajo lo detectar\u00eda.<\/p>\n<h2>Ejercicio 1: Firma Local con Par de Claves<\/h2>\n<p>Antes de pasar a la firma keyless en CI, es \u00fatil entender los fundamentos firmando una imagen localmente con un par de claves expl\u00edcito.<\/p>\n<h3>Paso 1 \u2014 Generar un par de claves de Cosign<\/h3>\n<pre><code>cosign generate-key-pair<\/code><\/pre>\n<p>Esto crea dos archivos en tu directorio actual:<\/p>\n<ul>\n<li><code>cosign.key<\/code> \u2014 la clave privada (cifrada con una contrase\u00f1a que elijas).<\/li>\n<li><code>cosign.pub<\/code> \u2014 la clave p\u00fablica que distribuyes a los verificadores.<\/li>\n<\/ul>\n<h3>Paso 2 \u2014 Construir, publicar y firmar la imagen<\/h3>\n<pre><code># Build the image\ndocker build -t ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1 .\n\n# Push to GHCR\ndocker push ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1\n\n# Sign the image with your private key\ncosign sign --key cosign.key ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1<\/code><\/pre>\n<p>Reemplaza <code>&lt;your-username&gt;<\/code> con tu nombre de usuario de GitHub. Cosign te pedir\u00e1 la contrase\u00f1a que estableciste durante la generaci\u00f3n de claves.<\/p>\n<h3>Paso 3 \u2014 Verificar la firma<\/h3>\n<pre><code>cosign verify --key cosign.pub ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1<\/code><\/pre>\n<p>Deber\u00edas ver una salida similar a:<\/p>\n<pre><code>Verification for ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1 --\nThe following checks were performed on each of these signatures:\n  - The cosign claims were validated\n  - The signatures were verified against the specified public key\n\n[{\"critical\":{\"identity\":{\"docker-reference\":\"ghcr.io\/&lt;your-username&gt;\/cosign-lab\"},\"image\":{\"docker-manifest-digest\":\"sha256:abc123...\"},\"type\":\"cosign container image signature\"},\"optional\":null}]<\/code><\/pre>\n<h3>\u00bfD\u00f3nde se almacena la firma?<\/h3>\n<p>Cosign almacena las firmas como artefactos OCI en el mismo registro, junto a la imagen. Para una imagen etiquetada como <code>sha256:abc123<\/code>, Cosign publica la firma en un tag derivado de ese digest \u2014 <code>sha256-abc123.sig<\/code>. Esto significa:<\/p>\n<ul>\n<li>No se necesita infraestructura separada para almacenar firmas.<\/li>\n<li>Las firmas viajan con la imagen cuando replicas registros.<\/li>\n<li>Los controles de acceso del registro se aplican a las firmas de la misma manera que a las im\u00e1genes.<\/li>\n<\/ul>\n<p>La firma con par de claves funciona, pero introduce una carga de gesti\u00f3n de claves: debes proteger la clave privada, rotarla peri\u00f3dicamente y distribuir la clave p\u00fablica a cada verificador. En el siguiente ejercicio, eliminamos esa carga por completo con la firma keyless.<\/p>\n<h2>Ejercicio 2: Firma Keyless en GitHub Actions<\/h2>\n<p>La firma keyless elimina la necesidad de generar, almacenar o rotar claves de firma. En su lugar, se basa en certificados de corta duraci\u00f3n emitidos por <strong>Fulcio<\/strong> y registrados en el log de transparencia <strong>Rekor<\/strong>.<\/p>\n<h3>C\u00f3mo funciona la firma keyless<\/h3>\n<ol>\n<li><strong>Token OIDC<\/strong> \u2014 GitHub Actions genera un token de identidad OIDC que prueba la identidad del workflow (repositorio, archivo de workflow, ref, y m\u00e1s).<\/li>\n<li><strong>Certificado Fulcio<\/strong> \u2014 Cosign env\u00eda ese token OIDC a Fulcio, que emite un certificado X.509 de firma de corta duraci\u00f3n vinculado a la identidad del workflow.<\/li>\n<li><strong>Firma<\/strong> \u2014 Cosign firma el digest de la imagen con la clave privada ef\u00edmera que corresponde al certificado Fulcio.<\/li>\n<li><strong>Log de transparencia Rekor<\/strong> \u2014 La firma y el certificado se registran en Rekor para que cualquiera pueda auditar cu\u00e1ndo y por qui\u00e9n se firm\u00f3 una imagen.<\/li>\n<li><strong>Eliminaci\u00f3n de la clave<\/strong> \u2014 La clave privada ef\u00edmera se descarta inmediatamente. La verificaci\u00f3n usa el certificado y la entrada de Rekor, no una clave p\u00fablica de larga duraci\u00f3n.<\/li>\n<\/ol>\n<h3>Paso 1 \u2014 Crear el workflow de firma<\/h3>\n<p>Crea <code>.github\/workflows\/sign.yml<\/code>:<\/p>\n<pre><code>name: Build, Push, and Sign\n\non:\n  push:\n    tags:\n      - 'v*'\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  build-and-sign:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n      id-token: write   # Required for keyless signing via OIDC\n\n    steps:\n      - name: Checkout code\n        uses: actions\/checkout@v4\n\n      - name: Install Cosign\n        uses: sigstore\/cosign-installer@v3\n\n      - name: Log in to GHCR\n        uses: docker\/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata\n        id: meta\n        uses: docker\/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}\/${{ env.IMAGE_NAME }}\n\n      - name: Build and push\n        id: build\n        uses: docker\/build-push-action@v5\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n\n      - name: Sign the image\n        run: |\n          cosign sign --yes \\\n            ${{ env.REGISTRY }}\/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}<\/code><\/pre>\n<h3>Detalles clave de este workflow<\/h3>\n<ul>\n<li><code>id-token: write<\/code> \u2014 este permiso permite al runner solicitar un token OIDC a GitHub, que Fulcio usa para emitir el certificado de firma.<\/li>\n<li><code>packages: write<\/code> \u2014 necesario para publicar la imagen y su firma en GHCR.<\/li>\n<li><code>cosign sign --yes<\/code> \u2014 el flag <code>--yes<\/code> confirma el modo no interactivo (sin solicitud de consentimiento para keyless). La ausencia del flag <code>--key<\/code> significa que Cosign usa autom\u00e1ticamente la firma keyless.<\/li>\n<li>Firmamos por digest (<code>@sha256:...<\/code>) en lugar de por tag para asegurar que firmamos exactamente la imagen que acabamos de construir.<\/li>\n<\/ul>\n<h3>Paso 2 \u2014 Push y activar el workflow<\/h3>\n<pre><code>git add .github\/workflows\/sign.yml\ngit commit -m \"Add keyless signing workflow\"\ngit push origin main\ngit tag v1.0.0\ngit push origin v1.0.0<\/code><\/pre>\n<h3>Paso 3 \u2014 Revisar los logs de Actions<\/h3>\n<p>En el paso \u00abSign the image\u00bb ver\u00e1s una salida similar a:<\/p>\n<pre><code>Generating ephemeral keys...\nRetrieving signed certificate...\n\n        The sigstore community wants to hear from you! Connect with us at\n        https:\/\/links.sigstore.dev\/slack-invite\n\nSuccessfully verified SCT...\ntlog entry created with index: 45678901\nPushing signature to: ghcr.io\/&lt;your-username&gt;\/cosign-lab:sha256-a1b2c3d4.sig<\/code><\/pre>\n<p>La imagen ahora est\u00e1 firmada con un certificado que la vincula criptogr\u00e1ficamente a la identidad de tu workflow de GitHub Actions. La firma y el certificado quedan registrados permanentemente en el log de transparencia Rekor.<\/p>\n<h2>Ejercicio 3: Verificaci\u00f3n de Firmas Localmente<\/h2>\n<p>Verificar una imagen firmada con keyless requiere dos datos: la <strong>identidad del certificado<\/strong> (qui\u00e9n firm\u00f3) y el <strong>emisor OIDC<\/strong> (qui\u00e9n respald\u00f3 esa identidad).<\/p>\n<h3>Paso 1 \u2014 Verificar la imagen firmada<\/h3>\n<pre><code>cosign verify \\\n  --certificate-identity \"https:\/\/github.com\/&lt;your-username&gt;\/cosign-lab\/.github\/workflows\/sign.yml@refs\/tags\/v1.0.0\" \\\n  --certificate-oidc-issuer \"https:\/\/token.actions.githubusercontent.com\" \\\n  ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1.0.0<\/code><\/pre>\n<p>Salida exitosa:<\/p>\n<pre><code>Verification for ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1.0.0 --\nThe following checks were performed on each of these signatures:\n  - The cosign claims were validated\n  - Existence of the claims in the transparency log was verified offline\n  - The code-signing certificate was verified using trusted certificate authority\n  - The signatures were verified against the specified public key\n  - The signature was verified against a valid Fulcio certificate\n\n[{\"critical\":{\"identity\":{\"docker-reference\":\"ghcr.io\/&lt;your-username&gt;\/cosign-lab\"},\"image\":{\"docker-manifest-digest\":\"sha256:a1b2c3d4...\"},\"type\":\"cosign container image signature\"},\"optional\":{...}}]<\/code><\/pre>\n<h3>Paso 2 \u2014 Verificar con una identidad incorrecta (se espera fallo)<\/h3>\n<pre><code>cosign verify \\\n  --certificate-identity \"https:\/\/github.com\/attacker\/malicious-repo\/.github\/workflows\/build.yml@refs\/tags\/v1.0.0\" \\\n  --certificate-oidc-issuer \"https:\/\/token.actions.githubusercontent.com\" \\\n  ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1.0.0<\/code><\/pre>\n<p>Salida:<\/p>\n<pre><code>Error: no matching signatures:\nnone of the expected identities matched what was in the certificate<\/code><\/pre>\n<p>Esto confirma que las firmas est\u00e1n vinculadas a la identidad. Incluso si alguien logra publicar una firma, no pasar\u00e1 la verificaci\u00f3n a menos que haya sido firmada por el workflow exacto que especifiques.<\/p>\n<h3>Paso 3 \u2014 Verificar una imagen sin firmar (se espera fallo)<\/h3>\n<pre><code>cosign verify \\\n  --certificate-identity \"https:\/\/github.com\/&lt;your-username&gt;\/cosign-lab\/.github\/workflows\/sign.yml@refs\/tags\/v0.1.0\" \\\n  --certificate-oidc-issuer \"https:\/\/token.actions.githubusercontent.com\" \\\n  ghcr.io\/&lt;your-username&gt;\/cosign-lab:v0.1.0<\/code><\/pre>\n<p>Salida:<\/p>\n<pre><code>Error: no matching signatures\nno signatures found for image<\/code><\/pre>\n<p>La imagen v0.1.0 fue construida con el workflow sin firma de la secci\u00f3n de Configuraci\u00f3n del Entorno, por lo que no existe ninguna firma.<\/p>\n<h3>Comprendiendo los campos del certificado<\/h3>\n<p>Cuando verificas una firma keyless, Cosign comprueba varios campos incrustados en el certificado Fulcio:<\/p>\n<ul>\n<li><strong>Emisor<\/strong> (<code>certificate-oidc-issuer<\/code>) \u2014 el proveedor OIDC que autentic\u00f3 al firmante. Para GitHub Actions siempre es <code>https:\/\/token.actions.githubusercontent.com<\/code>.<\/li>\n<li><strong>Sujeto \/ Identidad<\/strong> (<code>certificate-identity<\/code>) \u2014 la referencia completa del workflow incluyendo el repositorio, la ruta del archivo de workflow y la referencia Git. Esto vincula la firma a un workflow espec\u00edfico en un commit o tag espec\u00edfico.<\/li>\n<li><strong>Extensiones de GitHub Workflow<\/strong> \u2014 el certificado tambi\u00e9n contiene extensiones OID personalizadas para el repositorio, el SHA del workflow, el evento desencadenante y el entorno del runner. Estas permiten pol\u00edticas de verificaci\u00f3n detalladas.<\/li>\n<\/ul>\n<p>Tambi\u00e9n puedes usar coincidencia por regex para pol\u00edticas m\u00e1s flexibles:<\/p>\n<pre><code>cosign verify \\\n  --certificate-identity-regexp \"https:\/\/github.com\/&lt;your-username&gt;\/cosign-lab\/.*\" \\\n  --certificate-oidc-issuer \"https:\/\/token.actions.githubusercontent.com\" \\\n  ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1.0.0<\/code><\/pre>\n<p>Esto es \u00fatil cuando quieres aceptar firmas de cualquier workflow en un repositorio, o de cualquier tag.<\/p>\n<h2>Ejercicio 4: Verificaci\u00f3n en Kubernetes con Kyverno<\/h2>\n<p>La verificaci\u00f3n local es \u00fatil para depuraci\u00f3n, pero los cl\u00fasteres de producci\u00f3n necesitan aplicaci\u00f3n automatizada. Kyverno es un controlador de admisi\u00f3n de Kubernetes que puede verificar firmas de Cosign en cada solicitud de admisi\u00f3n de pods.<\/p>\n<h3>Paso 1 \u2014 Instalar Kyverno<\/h3>\n<pre><code>helm repo add kyverno https:\/\/kyverno.github.io\/kyverno\/\nhelm repo update\nhelm install kyverno kyverno\/kyverno -n kyverno --create-namespace<\/code><\/pre>\n<p>Espera a que los pods de Kyverno est\u00e9n listos:<\/p>\n<pre><code>kubectl wait --for=condition=ready pod -l app.kubernetes.io\/instance=kyverno -n kyverno --timeout=120s<\/code><\/pre>\n<h3>Paso 2 \u2014 Crear la pol\u00edtica de verificaci\u00f3n de im\u00e1genes<\/h3>\n<p>Guarda lo siguiente como <code>require-signed-images.yml<\/code>:<\/p>\n<pre><code>apiVersion: kyverno.io\/v1\nkind: ClusterPolicy\nmetadata:\n  name: require-cosign-signature\nspec:\n  validationFailureAction: Enforce\n  background: false\n  rules:\n    - name: verify-cosign-signature\n      match:\n        any:\n          - resources:\n              kinds:\n                - Pod\n      verifyImages:\n        - imageReferences:\n            - \"ghcr.io\/&lt;your-username&gt;\/cosign-lab:*\"\n          attestors:\n            - entries:\n                - keyless:\n                    subject: \"https:\/\/github.com\/&lt;your-username&gt;\/cosign-lab\/.github\/workflows\/sign.yml@refs\/tags\/*\"\n                    issuer: \"https:\/\/token.actions.githubusercontent.com\"\n                    rekor:\n                      url: \"https:\/\/rekor.sigstore.dev\"<\/code><\/pre>\n<p>Aplica la pol\u00edtica:<\/p>\n<pre><code>kubectl apply -f require-signed-images.yml<\/code><\/pre>\n<h3>Paso 3 \u2014 Probar con una imagen firmada (deber\u00eda tener \u00e9xito)<\/h3>\n<pre><code>kubectl run signed-app \\\n  --image=ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1.0.0 \\\n  --restart=Never<\/code><\/pre>\n<p>Salida esperada:<\/p>\n<pre><code>pod\/signed-app created<\/code><\/pre>\n<h3>Paso 4 \u2014 Probar con una imagen sin firmar (deber\u00eda fallar)<\/h3>\n<pre><code>kubectl run unsigned-app \\\n  --image=ghcr.io\/&lt;your-username&gt;\/cosign-lab:v0.1.0 \\\n  --restart=Never<\/code><\/pre>\n<p>Salida esperada:<\/p>\n<pre><code>Error from server: admission webhook \"mutate.kyverno.svc-fail\" denied the request:\nresource Pod\/default\/unsigned-app was blocked due to the following policies:\n\nrequire-cosign-signature:\n  verify-cosign-signature: |\n    image verification failed for ghcr.io\/&lt;your-username&gt;\/cosign-lab:v0.1.0:\n    no matching signatures found<\/code><\/pre>\n<p>Este es exactamente el bucle de aplicaci\u00f3n que deseas: solo las im\u00e1genes firmadas por tu workflow confiable de GitHub Actions pueden entrar al cl\u00faster.<\/p>\n<h2>Ejercicio 5: Adjuntar un SBOM<\/h2>\n<p>Una firma demuestra qui\u00e9n construy\u00f3 la imagen. Una attestation de SBOM demuestra qu\u00e9 contiene. Combinar ambas te da una cadena de confianza completa: identidad, integridad y transparencia del contenido.<\/p>\n<h3>Paso 1 \u2014 Generar el SBOM con Syft<\/h3>\n<pre><code>syft ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1.0.0 -o spdx-json > sbom.spdx.json<\/code><\/pre>\n<p>Esto escanea las capas de la imagen y produce un documento JSON en formato SPDX que lista cada paquete, biblioteca y dependencia dentro de la imagen.<\/p>\n<h3>Paso 2 \u2014 Adjuntar el SBOM como attestation<\/h3>\n<pre><code>cosign attest \\\n  --predicate sbom.spdx.json \\\n  --type spdxjson \\\n  --yes \\\n  ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1.0.0<\/code><\/pre>\n<p>Al igual que la firma keyless, esto usa identidad basada en OIDC cuando se ejecuta en GitHub Actions o solicita autenticaci\u00f3n interactiva cuando se ejecuta localmente. La attestation se almacena como un artefacto OCI junto a la imagen.<\/p>\n<h3>Paso 3 \u2014 Verificar la attestation<\/h3>\n<pre><code>cosign verify-attestation \\\n  --type spdxjson \\\n  --certificate-identity \"https:\/\/github.com\/&lt;your-username&gt;\/cosign-lab\/.github\/workflows\/sign.yml@refs\/tags\/v1.0.0\" \\\n  --certificate-oidc-issuer \"https:\/\/token.actions.githubusercontent.com\" \\\n  ghcr.io\/&lt;your-username&gt;\/cosign-lab:v1.0.0<\/code><\/pre>\n<p>La verificaci\u00f3n exitosa confirma que el SBOM fue generado y adjuntado por tu workflow confiable, y que no ha sido manipulado desde entonces.<\/p>\n<h2>El Pipeline Completo de Firma<\/h2>\n<p>Aqu\u00ed est\u00e1 el workflow final que combina todo: construir, publicar, firmar, generar un SBOM y certificarlo. Gu\u00e1rdalo como <code>.github\/workflows\/sign-and-attest.yml<\/code>:<\/p>\n<pre><code>name: Build, Sign, and Attest\n\non:\n  push:\n    tags:\n      - 'v*'\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  build-sign-attest:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n      id-token: write\n\n    steps:\n      - name: Checkout code\n        uses: actions\/checkout@v4\n\n      - name: Install Cosign\n        uses: sigstore\/cosign-installer@v3\n\n      - name: Install Syft\n        uses: anchore\/sbom-action\/download-syft@v0\n\n      - name: Log in to GHCR\n        uses: docker\/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata\n        id: meta\n        uses: docker\/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}\/${{ env.IMAGE_NAME }}\n\n      - name: Build and push\n        id: build\n        uses: docker\/build-push-action@v5\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n\n      - name: Sign the image\n        run: |\n          cosign sign --yes \\\n            ${{ env.REGISTRY }}\/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}\n\n      - name: Generate SBOM\n        run: |\n          syft ${{ env.REGISTRY }}\/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} \\\n            -o spdx-json > sbom.spdx.json\n\n      - name: Attest SBOM\n        run: |\n          cosign attest --yes \\\n            --predicate sbom.spdx.json \\\n            --type spdxjson \\\n            ${{ env.REGISTRY }}\/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}\n\n      - name: Verify signature\n        run: |\n          cosign verify \\\n            --certificate-identity-regexp \"https:\/\/github.com\/${{ github.repository }}\/.*\" \\\n            --certificate-oidc-issuer \"https:\/\/token.actions.githubusercontent.com\" \\\n            ${{ env.REGISTRY }}\/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}\n\n      - name: Verify SBOM attestation\n        run: |\n          cosign verify-attestation \\\n            --type spdxjson \\\n            --certificate-identity-regexp \"https:\/\/github.com\/${{ github.repository }}\/.*\" \\\n            --certificate-oidc-issuer \"https:\/\/token.actions.githubusercontent.com\" \\\n            ${{ env.REGISTRY }}\/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}<\/code><\/pre>\n<p>Este workflow te da una cadena de confianza completa para cada release etiquetado: la imagen est\u00e1 firmada, su contenido est\u00e1 documentado en un SBOM, y el SBOM est\u00e1 certificado criptogr\u00e1ficamente \u2014 todo sin gestionar una sola clave de larga duraci\u00f3n.<\/p>\n<h2>Limpieza<\/h2>\n<p>Cuando hayas terminado con el laboratorio, limpia los recursos que creaste.<\/p>\n<h3>Eliminar im\u00e1genes de prueba de GHCR<\/h3>\n<p>Navega a <code>https:\/\/github.com\/&lt;your-username&gt;?tab=packages<\/code> y elimina el paquete <code>cosign-lab<\/code>, o usa el GitHub CLI:<\/p>\n<pre><code># List package versions\ngh api user\/packages\/container\/cosign-lab\/versions | jq '.[].id'\n\n# Delete each version\ngh api --method DELETE user\/packages\/container\/cosign-lab\/versions\/&lt;version-id&gt;<\/code><\/pre>\n<h3>Eliminar Kyverno<\/h3>\n<pre><code>kubectl delete clusterpolicy require-cosign-signature\nhelm uninstall kyverno -n kyverno\nkubectl delete namespace kyverno<\/code><\/pre>\n<h3>Eliminar el repositorio de prueba<\/h3>\n<pre><code>gh repo delete &lt;your-username&gt;\/cosign-lab --yes<\/code><\/pre>\n<h3>Eliminar archivos locales<\/h3>\n<pre><code>cd .. && rm -rf cosign-lab\nrm -f cosign.key cosign.pub<\/code><\/pre>\n<h2>Conclusiones Clave<\/h2>\n<ul>\n<li><strong>La firma keyless elimina la gesti\u00f3n de claves.<\/strong> Al usar tokens de identidad OIDC de GitHub Actions y certificados Fulcio de corta duraci\u00f3n, evitas la carga operativa de generar, almacenar, rotar y distribuir claves de firma.<\/li>\n<li><strong>Las firmas est\u00e1n vinculadas a la identidad, no a la clave.<\/strong> La verificaci\u00f3n comprueba qui\u00e9n firm\u00f3 la imagen (qu\u00e9 workflow, en qu\u00e9 repositorio, en qu\u00e9 ref) en lugar de qu\u00e9 clave se us\u00f3. Esto hace que las pol\u00edticas sean m\u00e1s intuitivas y auditables.<\/li>\n<li><strong>El log de transparencia Rekor proporciona un rastro de auditor\u00eda a prueba de manipulaciones.<\/strong> Cada firma se registra p\u00fablicamente, por lo que puedes demostrar cu\u00e1ndo se firm\u00f3 una imagen y detectar cualquier intento de antedatar o eliminar firmas.<\/li>\n<li><strong>Los controladores de admisi\u00f3n aplican pol\u00edticas de firma en tiempo de despliegue.<\/strong> Kyverno (o alternativas como Connaisseur o Sigstore Policy Controller) asegura que las im\u00e1genes sin firmar o firmadas incorrectamente nunca se ejecuten en tu cl\u00faster.<\/li>\n<li><strong>Las attestations de SBOM extienden la cadena de confianza.<\/strong> La firma demuestra qui\u00e9n construy\u00f3 la imagen; adjuntar un SBOM firmado demuestra qu\u00e9 contiene. Juntos, proporcionan procedencia completa desde el c\u00f3digo fuente hasta el tiempo de ejecuci\u00f3n.<\/li>\n<li><strong>Firma por digest, no por tag.<\/strong> Los tags son mutables \u2014 alguien puede mover un tag a una imagen diferente. Los digests son direcciones de contenido inmutables, por lo que firmar por digest garantiza que firmaste exactamente la imagen que construiste.<\/li>\n<\/ul>\n<h2>Pr\u00f3ximos Pasos<\/h2>\n<p>Contin\u00faa construyendo tu conocimiento en seguridad de la cadena de suministro con estas gu\u00edas relacionadas:<\/p>\n<ul>\n<li><a href=\"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/signing-verifying-container-images-sigstore-cosign\/\">Firma y Verificaci\u00f3n de Im\u00e1genes de Contenedor con Sigstore y Cosign<\/a> \u2014 una gu\u00eda completa que cubre la arquitectura de Cosign, patrones avanzados de verificaci\u00f3n e integraci\u00f3n con diferentes registros y sistemas de CI.<\/li>\n<li><a href=\"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/artifact-provenance-attestations-slsa-in-toto\/\">Procedencia de Artefactos y Attestations: De SLSA a in-toto<\/a> \u2014 comprende el ecosistema m\u00e1s amplio de procedencia incluyendo los niveles SLSA, layouts de in-toto, y c\u00f3mo las attestations encajan en una estrategia completa de seguridad de la cadena de suministro.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Descripci\u00f3n General Cada imagen de contenedor que produce tu pipeline de CI\/CD debe estar firmada criptogr\u00e1ficamente antes de llegar a cualquier entorno. Las im\u00e1genes sin firmar son un punto ciego \u2014 no tienes prueba de que provengan de tu pipeline, no tienes garant\u00eda de que no hayan sido manipuladas en tr\u00e1nsito, y no tienes ning\u00fan &#8230; <a title=\"Lab: Firma y Verificaci\u00f3n de Im\u00e1genes Container con Cosign en GitHub Actions\" class=\"read-more\" href=\"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/lab-signing-verifying-container-images-cosign-github-actions\/\" aria-label=\"Leer m\u00e1s sobre Lab: Firma y Verificaci\u00f3n de Im\u00e1genes Container con Cosign en GitHub Actions\">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,56,59],"tags":[],"post_folder":[],"class_list":["post-661","post","type-post","status-publish","format-standard","hentry","category-ci-cd-security","category-github-actions","category-software-supply-chain"],"_links":{"self":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts\/661","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=661"}],"version-history":[{"count":1,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts\/661\/revisions"}],"predecessor-version":[{"id":678,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts\/661\/revisions\/678"}],"wp:attachment":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/media?parent=661"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/categories?post=661"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/tags?post=661"},{"taxonomy":"post_folder","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/post_folder?post=661"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}