Módulo 6: Supply Chain Sbom And Signing

5. Manos a la obra: instalando `cosign` y un keypair local

Descripción

Con la teoría de la lección 4 resuelta, esta lección instala cosign de verdad, confirma su versión, y genera el keypair local que las lecciones 6 y 7 van a usar para firmar y verificar lambda/function.zip. El paso que más sorprende la primera vez —y la razón por la que esta lección le dedica una sección completa— es que cosign generate-key-pair pide una contraseña de forma interactiva, algo que rompería cualquier intento de automatizar este paso dentro de un pipeline de CI. Esta lección muestra el mecanismo real para resolverlo sin bloquear nada, documentado con la salida literal de esta guía.

Conexión con el módulo

cosign.key y cosign.pub, generados aquí, son los dos archivos que las lecciones 6, 7 y 8 de este módulo van a usar en cada comando de firma y verificación. Es, junto con sbom.cyclonedx.json de la lección 3, uno de los tres artefactos nuevos que este módulo agrega a andes-cargo-infra/.


Paso 1 — Instalando cosign

brew install cosign

Qué esperar (resumen — la salida completa de brew varía según qué tenías ya instalado; el resultado que importa es el binario final):

==> Fetching downloads for: cosign
✔︎ Bottle cosign (3.1.3)
==> Would install 1 formula:
cosign
🍺  /opt/homebrew/Cellar/cosign/3.1.3: 12 files, 97.9MB

Si no usas macOS con Homebrew, la página de releases oficial de sigstore/cosign tiene binarios para Linux y Windows — el resto de esta lección, y de este módulo completo, no depende de cómo instalaste el binario, solo de que cosign quede disponible en tu PATH.

Confirma la versión exacta:

cosign version

Qué esperar (literal, ejecutado para escribir esta lección):

GitVersion:    v3.1.3
GitCommit:     11926fa5bbbbde47e88fc006b625a17769b743b2
GitTreeState:  "clean"
BuildDate:     2026-08-05T23:43:27Z
GoVersion:     go1.26.5
Compiler:      gc
Platform:      darwin/arm64

v3.1.3 — la misma versión confirmada en el diseño de esta guía, publicada el 6 de agosto de 2026 como parche de seguridad (GHSA-fx35-mq7g-6g98, sobre el manejo de un formato de bundle heredado). Si tu instalación reporta una versión mayor, no hay ningún problema: cada lección de este módulo describe el comportamiento observado con esta versión exacta, y cosign mantiene compatibilidad hacia adelante en los comandos que este módulo usa.


Paso 2 — Generando el keypair, y el problema real de la contraseña interactiva

El comando que genera el par de llaves es:

cosign generate-key-pair

Si lo corres exactamente así, en una terminal interactiva, vas a ver esto:

Enter password for private key:

cosign cifra la llave privada (cosign.key) con una contraseña antes de escribirla en disco — una capa adicional de protección: si alguien roba el archivo cosign.key sin conocer esa contraseña, no puede usarlo para firmar nada. Es una decisión de diseño razonable para el uso interactivo normal de la herramienta. El problema aparece en el contexto exacto de esta guía —y de cualquier pipeline de CI real—: un prompt interactivo que espera que alguien teclee algo no puede correr dentro de un script automatizado ni dentro de un job de GitHub Actions, donde no hay ningún humano frente a una terminal para escribir una contraseña en el momento exacto en que el comando la pide. Si corrieras este comando tal cual dentro de la lección 8 (que lo integra en apply.yml), el job se quedaría colgado esperando una entrada que nunca llega, hasta agotar el tiempo de espera del runner.


Paso 3 — La solución real: la variable de entorno COSIGN_PASSWORD

cosign reconoce la variable de entorno COSIGN_PASSWORD y, si está definida —incluso vacía—, la usa como contraseña sin mostrar ningún prompt interactivo:

COSIGN_PASSWORD="" cosign generate-key-pair

Qué esperar (literal, ejecutado para escribir esta lección — sin ningún prompt, sin ninguna espera):

Private key written to cosign.key
Public key written to cosign.pub

Dos líneas, ninguna pregunta, código de salida 0. Esta guía usa una contraseña vacía de forma deliberada, por la misma razón de honestidad que ya viste con AWS_ACCESS_KEY_ID=test/AWS_SECRET_ACCESS_KEY=test en terraform-and-iac-guide y cicd-and-gitops-on-aws-guide: es un laboratorio $0, sin secretos reales que proteger detrás de esta llave —el cosign.key de esta guía firma un .zip de práctica de 890 bytes, no un artefacto de producción de una empresa real—. En un proyecto real, COSIGN_PASSWORD seguiría siendo el mecanismo correcto para automatizar este paso, pero su valor vendría de un secreto gestionado —exactamente el mismo SSM Parameter Store/Secrets Manager que construiste en el Módulo 3, o el equivalente de secretos de GitHub Actions—, nunca de una cadena vacía escrita a mano en un script.


Paso 4 — Inspeccionando los dos archivos generados

ls -la cosign.key cosign.pub

Qué esperar (literal, ejecutado para escribir esta lección):

-rw-------  1 andes-cargo  staff   653 cosign.key
-rw-r--r--  1 andes-cargo  staff   178 cosign.pub

Fíjate en los permisos, no solo en los tamaños: cosign.key queda con permisos 600 (lectura/escritura solo para el propietario) por defecto — nadie más en el mismo sistema puede siquiera leer el archivo, una segunda capa de protección independiente de la contraseña que ya cifra su contenido. cosign.pub, en cambio, queda con permisos 644 (lectura para cualquiera) — coherente con su propósito: una llave pública existe, precisamente, para distribuirse sin restricción, exactamente lo contrario de la privada.

Mira el formato de cada archivo:

cat cosign.pub

Qué esperar (literal — el contenido matemático de la llave es único de cada corrida, pero el formato es siempre este):

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE42I8G6E1Zbwq+VSyEaL4QM6JU3Si
f6FM6IKkLRBPp0Yjy3wwL1Ehyakktmm9qByYGqQ4sTpj6OQ2OWsjFpLgEw==
-----END PUBLIC KEY-----

Un bloque PEM estándar (-----BEGIN PUBLIC KEY-----), el mismo formato que reconoce cualquier herramienta criptográfica que trabaje con llaves públicas — no hay nada propietario de Sigstore en este archivo, es una SubjectPublicKeyInfo de ECDSA sobre la curva P-256 (prime256v1), el algoritmo por defecto de cosign generate-key-pair.

head -1 cosign.key

Qué esperar (literal):

-----BEGIN ENCRYPTED SIGSTORE PRIVATE KEY-----

Distinto del encabezado genérico BEGIN EC PRIVATE KEY que verías en una llave PKI tradicional sin cifrar — ENCRYPTED SIGSTORE PRIVATE KEY confirma, en el propio encabezado del archivo, que el contenido está cifrado con la contraseña que definiste en el Paso 3, y que el formato interno es específico de Sigstore (no un PKCS#8 genérico), aunque envuelto en el mismo armazón PEM reconocible.


Paso 5 — Preparando cosign.key para que nunca se versione

cosign.pub es exactamente el tipo de archivo que quieres en el repositorio — es la llave que cualquiera necesita para verificar una firma tuya, y su exposición pública no compromete nada. cosign.key, en cambio, aunque está cifrado, nunca debería versionarse: es una capa de defensa, no una excusa para bajar la guardia. Agrégalo al .gitignore de la raíz de andes-cargo-infra/ —el mismo archivo que ya excluye lambda/function.zip, .terraform/ y terraform.tfstate desde módulos anteriores—:

echo "cosign.key" >> .gitignore
git status --short

Qué esperar (literal — asumiendo que cosign.pub sí queda para commitearse, y cosign.key desaparece de la lista de archivos rastreados):

 M .gitignore
?? cosign.pub

cosign.key no aparece en absoluto en git status —exactamente el comportamiento correcto de un .gitignore funcionando—, mientras que cosign.pub sí aparece como un archivo nuevo, listo para el próximo commit. Esta es la misma disciplina que terraform-and-iac-guide ya estableció con function.zip (generado, nunca versionado) y que secrets.tf del Módulo 3 estableció con cualquier valor de secreto real: lo que se puede regenerar o lo que nunca debe distribuirse, fuera del repositorio; lo que otros necesitan para verificar tu trabajo, dentro.


Errores comunes

Olvidar COSIGN_PASSWORD al automatizar este paso, y que el proceso se cuelgue sin ningún mensaje de error claro (el más importante de esta lección). Qué pasa: alguien copia cosign generate-key-pair a un script o a un job de CI sin la variable de entorno, y la ejecución simplemente no avanza —sin fallar, sin ningún mensaje, solo esperando indefinidamente—. Cómo detectarlo: si un script que debería terminar en segundos parece "colgado", sin ninguna salida nueva en la terminal. Cómo corregirlo: cualquier invocación de cosign generate-key-pair fuera de una sesión interactiva humana necesita COSIGN_PASSWORD definida en el entorno —vacía, como esta lección, o con un valor real gestionado por un secreto—, sin excepción.

Commitear cosign.key por accidente, junto con cosign.pub, antes de configurar el .gitignore (de secuencia, revisita el Módulo 3). Qué pasa: alguien genera el keypair antes de tocar .gitignore, y ambos archivos quedan como "sin rastrear" en git status — un git add . descuidado los versiona a los dos. Cómo detectarlo: git log --all --full-history -- cosign.key muestra algún commit, aunque hayas borrado el archivo después (la historia de Git no olvida). Cómo corregirlo: el Paso 5 de esta lección agrega cosign.key al .gitignore antes de cualquier git add, exactamente en el orden correcto — si ya lo commiteaste por error, el archivo cifrado en sí no es una llave utilizable sin la contraseña (una ventaja real de que esté cifrado), pero la práctica correcta sigue siendo regenerar un keypair nuevo y purgar el commit expuesto, no confiar en que el cifrado sea excusa suficiente para dejarlo en la historia.

Asumir que una contraseña vacía (COSIGN_PASSWORD="") es una mala práctica en cualquier contexto, sin distinguir este laboratorio de un caso real (de generalización excesiva). Qué pasa: alguien, viendo COSIGN_PASSWORD="" en esta lección, concluye que es la forma "correcta" de manejar este paso en cualquier proyecto. Cómo detectarlo: si copias este comando exacto a un proyecto con un artefacto de producción real, sin cambiar nada. Cómo corregirlo: una contraseña vacía es aceptable aquí exactamente por la misma razón que AWS_ACCESS_KEY_ID=test lo es en toda esta guía — nada real está protegido detrás de ella. En un proyecto con un artefacto de producción de verdad, COSIGN_PASSWORD debería venir de un gestor de secretos real (SSM Parameter Store, Secrets Manager, o el equivalente de GitHub Actions secrets.*), nunca de una cadena vacía escrita a mano — el mecanismo de automatización es el mismo; lo que cambia es de dónde sale el valor.


Ejercicios

Ejercicio 1 — Explica, sin mirar esta lección, por qué cosign.key tiene permisos 600 mientras que cosign.pub tiene 644. Un compañero, revisando el proyecto, pregunta por qué los dos archivos generados por el mismo comando tienen permisos distintos. ¿Qué le responderías?

Ver solución

cosign.key contiene la llave privada, cifrada con una contraseña —quien pueda leer ese archivo Y conozca la contraseña puede firmar en tu nombre—, así que restringir su lectura a solo el propietario del archivo (permisos 600) es una capa de defensa adicional, independiente del cifrado del contenido mismo: incluso otro usuario del mismo sistema operativo, sin la contraseña, no debería poder ni siquiera copiar el archivo. cosign.pub contiene la llave pública, cuyo propósito completo es que cualquiera pueda usarla para verificar una firma —restringir su lectura no aportaría ninguna seguridad adicional y solo estorbaría el caso de uso normal (compartirla ampliamente)—, así que cosign generate-key-pair la deja con permisos de lectura abiertos (644) por defecto.

Ejercicio 2 — Predice qué pasaría si corrieras cosign generate-key-pair una segunda vez, en el mismo directorio, sin borrar los archivos existentes. ¿Esperarías que cosign sobrescriba silenciosamente cosign.key/cosign.pub, o que haga algo distinto?

Ver solución

cosign detecta que los archivos ya existen y se niega a sobrescribirlos por defecto, para evitar que alguien pierda accidentalmente un keypair existente que podría estar en uso (por ejemplo, si manifest.sig de la lección 6 ya se generó con la llave anterior, sobrescribirla silenciosamente rompería la verificación de esa firma sin ningún aviso). El comando reporta un error indicando que los archivos ya existen, y hace falta un flag explícito o borrar los archivos a mano primero, para confirmar la intención de generar un keypair nuevo. Es el mismo principio de "nunca destruir silenciosamente" que ya viste en no-destroy-shipments.rego (Módulo 4) — aplicado aquí a un archivo local, no a un recurso de AWS.

Ejercicio 3 — Decide si esta lección necesitaba sudo en algún paso, y justifica. Revisando los cinco pasos de esta lección, ¿en algún momento hizo falta un permiso elevado de sistema operativo? ¿Por qué sí o por qué no?

Ver solución

No, ningún paso de esta lección necesitó sudo. La instalación con brew install cosign escribe dentro del directorio de Homebrew, propiedad del usuario actual en una instalación estándar de macOS; cosign generate-key-pair escribe archivos en el directorio de trabajo actual, también propiedad del usuario. Esto es consistente con el principio de mínimo privilegio que gobierna toda esta guía desde el Módulo 2: ninguna herramienta de este módulo necesita, ni debería necesitar, permisos de administrador del sistema operativo para cumplir su función — firmar un artefacto de software es una operación de usuario normal, no una operación de sistema.


Resumen y siguiente paso

En esta lección instalaste cosign v3.1.3, confirmado con cosign version, y generaste tu primer keypair local con cosign generate-key-pair — resolviendo, con COSIGN_PASSWORD="", el problema real de que ese comando pide una contraseña de forma interactiva por defecto, algo que bloquearía cualquier intento de automatizarlo. Inspeccionaste los dos archivos resultantes —cosign.key (653 bytes, permisos 600, formato ENCRYPTED SIGSTORE PRIVATE KEY) y cosign.pub (178 bytes, permisos 644, formato PUBLIC KEY estándar)— y aplicaste la misma disciplina de .gitignore que ya conoces de módulos anteriores: cosign.key nunca se versiona, cosign.pub sí.

Antes de avanzar deberías poder: explicar por qué COSIGN_PASSWORD es imprescindible para automatizar este paso; distinguir, por sus permisos de archivo y su contenido PEM, cuál de los dos archivos generados es seguro compartir; y justificar por qué una contraseña vacía es aceptable en este laboratorio específico, pero no lo sería en un proyecto con un artefacto de producción real.

Con el keypair listo, la lección 6 usa cosign.key para firmar lambda/function.zip de verdad, y cosign.pub para verificar esa firma — completamente offline, sin depender de Rekor.

Recursos

  1. Sigstore — Signing with a self-managed key — la documentación oficial de cosign generate-key-pair y el manejo de la contraseña, incluida la variable COSIGN_PASSWORD.
  2. GitHub — sigstore/cosign releases — la fuente de la versión v3.1.3 confirmada en esta lección, incluido el detalle del parche de seguridad de esa versión.
  3. Este curso, Módulo 3, secrets.tf — la misma disciplina de gestión de secretos (nunca en texto plano en el repositorio) aplicada aquí a un archivo de llave criptográfica en vez de una credencial de API.