diff --git a/.gitea/workflows/build-medusa.yaml b/.gitea/workflows/build-medusa.yaml index b644036..5e90458 100644 --- a/.gitea/workflows/build-medusa.yaml +++ b/.gitea/workflows/build-medusa.yaml @@ -287,12 +287,21 @@ jobs: # Login ya se hizo arriba; recién acá se sube, después de que la # imagen pasó el gate de CRITICAL. - name: Subir Imagen al Registry + id: push shell: bash run: | set -euo pipefail - docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" + PUSH_OUTPUT="$(docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}")" + echo "${PUSH_OUTPUT}" docker push "${IMAGE_NAME}:latest" + DIGEST="$(echo "${PUSH_OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)" + test -n "${DIGEST}" || { + echo "ERROR: no se pudo extraer el digest del push." + exit 1 + } + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + - name: Instalar Cosign shell: bash run: | @@ -310,7 +319,8 @@ jobs: # todo el registry de este lab. La llave pública se commitea en # cada directorio de app (workloads/commerce-backend/cosign.pub) # para que la verificación quede local a cada workflow, igual que - # el frontend. + # el frontend. Firma por digest (no por tag), mismo criterio que + # build.yaml. - name: Firmar Imagen (Cosign) shell: bash env: @@ -323,7 +333,7 @@ jobs: --use-signing-config=false \ --tlog-upload=false \ --yes \ - "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" + "${IMAGE_NAME}@${{ steps.push.outputs.digest }}" - name: Verificar Firma (smoke test) shell: bash @@ -332,7 +342,7 @@ jobs: /tmp/cosign verify \ --key "${APP_DIR}/cosign.pub" \ --insecure-ignore-tlog=true \ - "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" + "${IMAGE_NAME}@${{ steps.push.outputs.digest }}" - name: Verificar promoción segura id: promotion diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 25bab06..4d07696 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -426,12 +426,21 @@ jobs: # Login ya se hizo arriba; recién acá se sube, después de que la # imagen pasó el gate de CRITICAL. - name: Subir Imagen al Registry + id: push shell: bash run: | set -euo pipefail - docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" + PUSH_OUTPUT="$(docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}")" + echo "${PUSH_OUTPUT}" docker push "${IMAGE_NAME}:latest" + DIGEST="$(echo "${PUSH_OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)" + test -n "${DIGEST}" || { + echo "ERROR: no se pudo extraer el digest del push." + exit 1 + } + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + - name: Instalar Cosign shell: bash run: | @@ -443,12 +452,13 @@ jobs: chmod +x /tmp/cosign /tmp/cosign version - # Firma solo la versión (no :latest, que es un tag mutable y firmarlo - # pierde sentido en cuanto se vuelve a mover). --use-signing-config=false - # --tlog-upload=false: firma solo con el par de llaves propio, sin - # publicar metadata en el transparency log público de Sigstore — este - # registry es privado, no tiene sentido anunciar públicamente qué se - # firmó y cuándo. + # Firma por digest, no por tag: :VERSION y :latest son punteros que + # en teoría podrían volver a moverse, pero el digest identifica el + # contenido exacto de la imagen que Trivy ya escaneó unos pasos + # atrás. --use-signing-config=false --tlog-upload=false: firma solo + # con el par de llaves propio, sin publicar metadata en el + # transparency log público de Sigstore — este registry es privado, + # no tiene sentido anunciar públicamente qué se firmó y cuándo. - name: Firmar Imagen (Cosign) shell: bash env: @@ -461,7 +471,7 @@ jobs: --use-signing-config=false \ --tlog-upload=false \ --yes \ - "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" + "${IMAGE_NAME}@${{ steps.push.outputs.digest }}" # Smoke test: confirma en el propio pipeline que la firma que se # acaba de crear valida contra la llave pública commiteada en el @@ -475,7 +485,7 @@ jobs: /tmp/cosign verify \ --key "${APP_DIR}/cosign.pub" \ --insecure-ignore-tlog=true \ - "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" + "${IMAGE_NAME}@${{ steps.push.outputs.digest }}" # Evita que una ejecución antigua actualice frontend.yaml después de que # ya exista un commit más reciente en main. diff --git a/.gitea/workflows/deploy-docs.yaml b/.gitea/workflows/deploy-docs.yaml index cafd898..f3e9915 100644 --- a/.gitea/workflows/deploy-docs.yaml +++ b/.gitea/workflows/deploy-docs.yaml @@ -282,12 +282,21 @@ jobs: # Login ya se hizo arriba; recién acá se sube, después de que la # imagen pasó el gate de CRITICAL. - name: Subir Imagen al Registry + id: push shell: bash run: | set -euo pipefail - docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" + PUSH_OUTPUT="$(docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}")" + echo "${PUSH_OUTPUT}" docker push "${IMAGE_NAME}:latest" + DIGEST="$(echo "${PUSH_OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)" + test -n "${DIGEST}" || { + echo "ERROR: no se pudo extraer el digest del push." + exit 1 + } + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + - name: Instalar Cosign shell: bash run: | @@ -301,7 +310,8 @@ jobs: # Mismo par de llaves que el frontend y commerce-backend (secrets # ya existentes a nivel de repo). Llave pública commiteada en - # workloads/docs-portal/cosign.pub. + # workloads/docs-portal/cosign.pub. Firma por digest (no por tag), + # mismo criterio que build.yaml. - name: Firmar Imagen (Cosign) shell: bash env: @@ -314,7 +324,7 @@ jobs: --use-signing-config=false \ --tlog-upload=false \ --yes \ - "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" + "${IMAGE_NAME}@${{ steps.push.outputs.digest }}" - name: Verificar Firma (smoke test) shell: bash @@ -323,7 +333,7 @@ jobs: /tmp/cosign verify \ --key "${APP_DIR}/cosign.pub" \ --insecure-ignore-tlog=true \ - "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" + "${IMAGE_NAME}@${{ steps.push.outputs.digest }}" - name: Verificar promoción segura id: promotion diff --git a/docs/devsecops/cosign.md b/docs/devsecops/cosign.md new file mode 100644 index 0000000..c076028 --- /dev/null +++ b/docs/devsecops/cosign.md @@ -0,0 +1,47 @@ +# Firma de imágenes con Cosign + +Los 3 pipelines que construyen y publican imágenes hacia `gitea.cruzcloud.net` +(frontend en `.gitea/workflows/build.yaml`, commerce-backend en +`build-medusa.yaml`, docs-portal en `deploy-docs.yaml`) firman la imagen con +Cosign (par de llaves propio, sin CA externa) inmediatamente después del +`docker push` y antes de que cualquier manifiesto pueda referenciarla. + +## Firma por digest, no por tag + +Desde 2026-08-16 los tres workflows firman y verifican por **digest** +(`${IMAGE_NAME}@sha256:...`), no por tag (`${IMAGE_NAME}:${VERSION}`). + +Motivo: `:VERSION` y `:latest` son punteros mutables — nada impide que en el +futuro un tag se vuelva a mover a otro contenido. El digest identifica el +contenido exacto de la imagen que Trivy ya escaneó unos pasos antes en el +mismo job, así que la firma queda atada a ese contenido específico, no al +nombre que apunta a él en un momento dado. + +El digest se captura parseando la salida de `docker push` (`sha256:[a-f0-9]{64}`) +inmediatamente después de subir la imagen, y se pasa como `steps.push.outputs.digest` +a los pasos de firma y verificación (smoke test) que corren a continuación en +el mismo job. + +## Rekor / transparency log: deshabilitado (decisión, no pendiente) + +`cosign sign` corre con `--use-signing-config=false --tlog-upload=false` en +los tres workflows — evaluado y confirmado el 2026-08-16, se mantiene así. + +Razones: +- El registry (`gitea.cruzcloud.net`) es privado, de un lab. No hay valor de + auditoría en anunciar públicamente en Rekor qué se firmó y cuándo. +- Subir al transparency log público requiere una llamada de red saliente por + build hacia `rekor.sigstore.dev`, atravesando el túnel de Cloudflare. Este + túnel ya tuvo un incidente de SNI/orden con Cosign (ver memoria de + infraestructura), así que sumar otra dependencia de red saliente por cada + build agrega superficie de fallo sin beneficio real en este contexto. +- Impacto en tiempo de pipeline: evitado por completo al no hacer la llamada. + +Si el registry deja de ser privado, o aparece un requisito de compliance +externo que exija prueba pública de firma, revisar esta decisión. + +## Llaves públicas + +Cada app commitea su llave pública junto al resto de sus manifiestos +(`workloads//cosign.pub`), para que la verificación (paso de smoke test +dentro del mismo workflow) sea local y no dependa de un servicio externo.