Files
apps-registry/workloads/docs-portal/docs/devsecops/cosign.md
T
devopsandClaude Sonnet 5 b0b832f142
Build and Push Docs Portal / Escaneo de Secretos (Gitleaks) (push) Successful in 36s
Build and Push Docs Portal / Construir y publicar Docs Portal (push) Successful in 6m18s
docs(docs-portal): reflejar fixes de MTU, gitea-runner y cosign digest/rekor
Nuevo playbook del blackhole MTU cross-node (diagnostico + fix + validacion).
Actualiza el playbook de contencion de recursos Gitea/runner de 'pendiente'
a 'aplicado'. Actualiza cosign.md: firma por digest ya implementada, Rekor
como decision tomada (no TODO). Corrige 2 links que quedaron rotos al
renombrar la seccion de limitaciones (validado con mkdocs build --strict).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
2026-08-16 23:07:26 -05:00

205 lines
9.9 KiB
Markdown

# 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<br/>(llave privada, secret)"]
B --> C["cosign verify<br/>(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
No se firma `:latest` en ningún caso — es un tag mutable, se re-apunta a
una imagen distinta en cada build, así que firmarlo no significaría nada
útil. Y desde 2026-08-16 (ver más abajo) tampoco se firma directamente
por el tag versionado — se firma por **digest**, que es la única
referencia que realmente identifica una imagen de forma inequívoca.
## Verificar manualmente
Con la llave pública del repo, cualquiera puede confirmar la firma de
una imagen sin necesitar acceso a nada privado. Apuntando al digest
exacto (lo que firma el pipeline hoy):
```bash
cosign verify \
--key workloads/ecommerce/cosign.pub \
--insecure-ignore-tlog=true \
gitea.cruzcloud.net/devops/ecommerce-frontend@sha256:<digest>
```
También funciona apuntando a un tag — Cosign lo resuelve al digest que
tenga en ese momento antes de verificar —, pero por eso mismo es menos
preciso: si el tag fue reasignado después de la firma original, estarías
verificando una imagen distinta de la firmada, sin que nada te avise.
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.
## Decisiones ya resueltas (antes eran TODO)
Dos preguntas abiertas al validar el pipeline end-to-end por primera vez,
ambas ya cerradas — se documentan igual, porque el *por qué* de cada
decisión sigue siendo útil.
### 1. Firma por digest, no por tag — implementado 2026-08-16
Hasta el 2026-08-15, el pipeline firmaba `ecommerce-frontend:v1.0.104` —
un **tag**, no un **digest** (`sha256:...`). Un tag es una etiqueta
mutable: nada impedía que, después de firmada la imagen, alguien (o un
bug en el propio pipeline) volviera a subir contenido distinto bajo el
mismo tag `v1.0.104`. La firma original seguiría "verificando" — porque
Cosign, al verificar por tag, resuelve el tag al digest que tenga *en
ese momento*, no al que tenía cuando se firmó.
Esto no era hipotético: el propio Cosign lo advertía en cada firma de
este pipeline:
```text
WARNING: Image reference gitea.cruzcloud.net/.../ecommerce-frontend:v1.0.102
uses a tag, not a digest, to identify the image to sign.
This can lead you to sign a different image than the intended one.
```
**Fix aplicado:** el pipeline ahora captura el digest real que devuelve
`docker push` (parseando `sha256:[a-f0-9]{64}` de la salida) y lo usa
tanto para firmar como para verificar —
`gitea.cruzcloud.net/devops/ecommerce-frontend@sha256:...` en vez de
`:v1.0.104` — en los tres pipelines (`ecommerce-frontend`,
`commerce-backend`, `docs-portal`). Esa advertencia de Cosign ya no
debería aparecer en los logs.
### 2. El transparency log (Rekor) se mantiene deshabilitado — decisión, no pendiente
Ya se explicó arriba, en la sección "Por qué `--tlog-upload=false`" de
esta misma página, la decisión de no publicar en el transparency log
para este registry privado. Vale la pena nombrar en simple qué es lo
que se está dejando afuera:
Un **transparency log** (Rekor, el de Sigstore) es un registro
público, append-only, criptográficamente verificable, de "quién firmó
qué imagen y cuándo" — pensalo como un libro contable público que
nadie puede editar ni borrar después de escrito, solo agregar filas
nuevas. Cualquiera puede consultar ese libro para confirmar de forma
independiente (sin confiar en el propio proyecto, ni en la llave
privada, ni en el registry) que una firma específica existió en un
momento específico.
Sin transparency log, la garantía que queda es más débil: *"esta firma
la generó quien tuviera la llave privada en el momento en que se
verificó"* — pero no hay ningún registro externo e inmutable que
demuestre *cuándo* se generó, ni una forma de detectar si alguien con
acceso a la llave privada firmó algo por fuera del pipeline sin que
quede rastro.
**Revisado explícitamente el 2026-08-16** (ya no es un TODO sin decidir,
es una elección consciente confirmada): se mantiene deshabilitado.
Motivos concretos para este lab — habilitarlo agregaría una llamada de
red saliente por build hacia `rekor.sigstore.dev`, atravesando el mismo
túnel de Cloudflare que ya tuvo un incidente de SNI/orden de reglas con
Cosign (ver el playbook de [realm HTTP y Cosign](../playbooks/incidente-cosign-realm-http-tunnel-2026-08.md)) — más
superficie de fallo por build, sin beneficio real, ya que el registry es
privado y no hay a quién auditar públicamente. Revisar esta decisión de
nuevo si el registry deja de ser exclusivamente privado, o si se suma
más de una persona con acceso a la llave de firma.
## 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.