# Cosign — firma de imágenes !!! info "Qué problema resuelve" Todo lo anterior en esta sección (Gitleaks, Trivy, Semgrep, SBOM) responde a la pregunta *"¿esta imagen es segura de construir?"*. Cosign responde una pregunta distinta, y que pasa **después**: *"la imagen que está corriendo ahora mismo en el cluster, ¿es exactamente la que armó el pipeline — o pudo haber sido reemplazada, modificada, o subida por otra vía?"*. ## Analogía simple Pensalo como el sello de cera en un sobre antiguo. Cualquiera puede leer la carta (la imagen es pública, cualquiera puede bajarla del registry) — eso Cosign no lo esconde. Lo que el sello garantiza es otra cosa: que la carta salió exactamente de donde dice que salió, y que nadie la abrió y volvió a cerrar en el camino. - **La llave privada** (guardada como secret de Gitea, nunca en el repo) es el sello físico — solo el pipeline de CI puede estampar una firma válida, porque solo él tiene el sello. - **La llave pública** (`workloads/ecommerce/cosign.pub`, commiteada sin problema — es pública a propósito) es la forma de reconocer el sello: cualquiera puede mirar la carta, ver el sello, y confirmar "sí, esto lo selló quien tiene la llave privada" — sin necesitar la llave privada para verificarlo. Si alguien sube una imagen distinta con el mismo tag, o modifica un solo byte de la imagen original, la firma deja de coincidir. No es que Cosign "detecte" la alteración activamente — es que la verificación simplemente falla, porque la firma fue calculada sobre el digest exacto de la imagen original. ## Cómo funciona en este pipeline ```mermaid flowchart LR A["docker push"] --> B["cosign sign
(llave privada, secret)"] B --> C["cosign verify
(llave pública, repo)"] C -- "firma válida" --> D["✅ pipeline termina OK"] C -- "firma inválida/ausente" --> X["❌ pipeline falla"] ``` 1. **Par de llaves**: generado una vez con `cosign generate-key-pair`, protegido por password. La privada (`cosign.key`) se subió como secret de Gitea Actions (`COSIGN_PRIVATE_KEY` + `COSIGN_PASSWORD`) — nunca se commiteó al repo, ni existe en el disco de este equipo después de subirla. La pública (`cosign.pub`) sí vive commiteada en `workloads/ecommerce/cosign.pub`, porque su función es poder compartirse. 2. **Firma**: después de subir la imagen al registry, el pipeline la firma con la llave privada (leída desde el secret vía `--key env://COSIGN_PRIVATE_KEY`, sin escribirla nunca a disco). 3. **Verificación (smoke test)**: en el mismo pipeline, inmediatamente después, se verifica la firma recién creada contra la llave pública del repo. Si algo salió mal (llave incorrecta, imagen corrupta), el pipeline falla ahí mismo — antes de que nadie más intente confiar en esa imagen. ## Por qué `--tlog-upload=false` Cosign, por defecto, publica cada firma en el *transparency log* público de Sigstore (Rekor) — un registro público, auditable, de "quién firmó qué y cuándo", pensado para proyectos open source donde esa transparencia es el punto. Este registry (`gitea.cruzcloud.net`) es privado; no tiene sentido — y sería una fuga de metadata innecesaria — anunciar públicamente que este lab construyó una imagen `v1.0.97` en tal fecha. Por eso el pipeline firma solo con el par de llaves propio, localmente, sin tocar el transparency log público (`--use-signing-config=false --tlog-upload=false` al firmar, `--insecure-ignore-tlog=true` al verificar). !!! warning "Trade-off consciente, no gratis" Sin transparency log, la garantía es "esta firma la generó quien tiene la llave privada" — pero no hay un registro público e inmutable de *cuándo* se generó cada firma. Para un registry privado de un lab personal, ese trade-off tiene sentido. Para un proyecto open source con más de una persona firmando, seguramente no. ## Qué NO se firma Solo se firma el tag versionado (`ecommerce-frontend:v1.0.X`), no `:latest`. `:latest` es un tag mutable — se re-apunta a una imagen distinta en cada build — así que firmarlo no significa nada útil: la firma quedaría asociada al digest de turno, y la siguiente build la volvería a mover. Cualquier verificación real de firma debería apuntar siempre a un tag de versión específico (o, mejor todavía, al digest exacto). ## Verificar manualmente Con la llave pública del repo, cualquiera puede confirmar la firma de una imagen sin necesitar acceso a nada privado: ```bash cosign verify \ --key workloads/ecommerce/cosign.pub \ --insecure-ignore-tlog=true \ gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.97 ``` Si la imagen fue firmada por este pipeline, el comando termina con `exit 0` y muestra el detalle de la firma. Si no — sea porque nunca se firmó, porque la firmó otra llave, o porque la imagen fue modificada después — termina con `exit 1` y un error explícito. ## Qué falta (a propósito, todavía) Hoy la verificación de firma corre como smoke test **dentro del mismo pipeline que la creó** — útil para confirmar que el mecanismo funciona, pero no impide que alguien despliegue manualmente una imagen sin firmar en el cluster. El siguiente paso natural, que **no** se implementó en esta primera vuelta, sería un *admission controller* en el cluster (ej. [Sigstore's policy-controller](https://docs.sigstore.dev/policy-controller/overview/) o [Kyverno](https://kyverno.io/policies/other/verify-images/verify-images/) con una política de verificación de imágenes) que rechace cualquier Pod cuya imagen no tenga una firma válida de `cosign.pub` — momento en el que Argo CD dejaría de poder desplegar una imagen sin firmar, no solo el pipeline de CI. ## Si la llave privada se compromete 1. Generar un par nuevo (`cosign generate-key-pair`). 2. Reemplazar `COSIGN_PRIVATE_KEY` y `COSIGN_PASSWORD` en los secrets de Gitea Actions del repo. 3. Reemplazar `workloads/ecommerce/cosign.pub` con la nueva llave pública, en un commit normal (no es secreto, no hace falta reescribir historial). 4. Las imágenes ya firmadas con la llave vieja **siguen verificando contra la llave vieja** — no se "invalidan" solas. Si se sospecha compromiso real, hay que decidir explícitamente qué imágenes ya desplegadas se consideran no confiables, no asumir que rotar la llave alcanza.