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
205 lines
9.9 KiB
Markdown
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.
|