Cosign es la herramienta del proyecto Sigstore para firmar y verificar imágenes de contenedor. La firma se guarda en el mismo registro que la imagen, de modo que cualquiera con la clave pública (o conociendo la identidad del firmante) puede comprobar que la imagen que va a desplegar es exactamente la que se construyó. En este tutorial instalarás Cosign en Ubuntu 24.04, firmarás una imagen con un par de claves propio, la verificarás y después automatizarás la firma sin claves (keyless) desde GitHub Actions.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor o equipo con Ubuntu 24.04 LTS (x86_64), por ejemplo un VPS de CubePath, con un usuario no root con privilegios
sudo. - Docker Engine instalado, para construir y publicar la imagen de prueba.
- Una cuenta en un registro OCI con permisos de escritura. Los ejemplos usan GitHub Container Registry (
ghcr.io) con un token personal con el permisowrite:packages; funciona igual con Docker Hub, Harbor o cualquier registro compatible. - Para la parte keyless: un repositorio en GitHub con GitHub Actions habilitado.
En los comandos, sustituye your_user por tu usuario u organización de GitHub, en minúsculas.
Paso 1: Instalar Cosign
Cosign se distribuye como un binario estático. Descarga la última versión y el archivo de sumas de comprobación:
cd /tmp
curl -fsSLO https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
curl -fsSLO https://github.com/sigstore/cosign/releases/latest/download/cosign_checksums.txt
Verifica la integridad del binario antes de instalarlo:
sha256sum --ignore-missing -c cosign_checksums.txt
cosign-linux-amd64: OK
Instálalo en /usr/local/bin:
sudo install -m 0755 cosign-linux-amd64 /usr/local/bin/cosign
cosign version
GitVersion: v3.0.2
GitTreeState: clean
...
En servidores arm64 usa cosign-linux-arm64 en lugar de cosign-linux-amd64.
Paso 2: Publicar una imagen de prueba
Cosign firma imágenes que ya están en un registro. Inicia sesión en GHCR (Docker guarda las credenciales en ~/.docker/config.json y Cosign las reutiliza):
docker login ghcr.io -u your_user
Crea una imagen mínima y publícala:
mkdir -p ~/demo-app && cd ~/demo-app
printf 'FROM nginx:1.27-alpine\n' > Dockerfile
docker build -t ghcr.io/your_user/demo-app:v1.0.0 .
docker push ghcr.io/your_user/demo-app:v1.0.0
Obtén la referencia por digest. Los tags son mutables (alguien puede volver a publicar v1.0.0 con otro contenido), así que siempre se firma el digest:
IMAGE=$(docker inspect --format '{{index .RepoDigests 0}}' ghcr.io/your_user/demo-app:v1.0.0)
echo "$IMAGE"
ghcr.io/your_user/demo-app@sha256:4f1c3e8a9b2d...
Paso 3: Generar un par de claves
Genera las claves en un directorio que no forme parte de ningún repositorio Git:
mkdir -p ~/cosign && cd ~/cosign
cosign generate-key-pair
Cosign pide una contraseña para cifrar la clave privada. Usa una contraseña fuerte y guárdala en tu gestor de secretos:
Enter password for private key:
Enter password for private key again:
Private key written to cosign.key
Public key written to cosign.pub
cosign.key: clave privada cifrada. No la subas nunca a un repositorio.cosign.pub: clave pública. Puedes distribuirla libremente a quien tenga que verificar.
Restringe los permisos de la clave privada:
chmod 600 ~/cosign/cosign.key
Consejoen producción es preferible que la clave privada no salga de un KMS. Cosign admite claves en AWS KMS, Google Cloud KMS, Azure Key Vault y HashiCorp Vault, por ejemplo con
cosign generate-key-pair --kms awskms:///alias/cosign.
Paso 4: Firmar la imagen
Firma la imagen por digest con la clave privada. Puedes añadir anotaciones que quedan dentro del contenido firmado, como el commit de origen:
cosign sign --yes --key ~/cosign/cosign.key \
-a git-commit=abc1234 \
"$IMAGE"
Cosign pide la contraseña de la clave, sube la firma al registro junto a la imagen y registra una entrada en Rekor, el registro de transparencia público de Sigstore. --yes acepta esa confirmación sin preguntar.
Importantela entrada de Rekor es pública y contiene el digest de la imagen y la firma. Si la existencia de tus imágenes es confidencial, valóralo antes de usar la instancia pública.
Paso 5: Verificar la firma
Cualquier equipo con Cosign y la clave pública puede verificar la imagen, tanto por digest como por tag:
cosign verify --key ~/cosign/cosign.pub ghcr.io/your_user/demo-app:v1.0.0
Verification for ghcr.io/your_user/demo-app:v1.0.0 --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The signatures were verified against the specified public key
[{"critical":{"identity":{"docker-reference":"ghcr.io/your_user/demo-app"},"image":{"docker-manifest-digest":"sha256:4f1c3e8a9b2d..."},"type":"cosign container image signature"},"optional":{"git-commit":"abc1234"}}]
Para exigir además que la firma lleve una anotación concreta:
cosign verify --key ~/cosign/cosign.pub -a git-commit=abc1234 ghcr.io/your_user/demo-app:v1.0.0
Comprueba también que la verificación falla con una imagen que no has firmado tú, por ejemplo la imagen base:
cosign verify --key ~/cosign/cosign.pub nginx:1.27-alpine
Error: no signatures found
Si la imagen tuviera firmas hechas con otra clave, el error sería no matching signatures. La firma pertenece al contenido (el digest), no al tag: si reconstruyes la imagen con cualquier cambio y la publicas con el mismo tag, el digest cambia y la verificación falla hasta que la vuelvas a firmar. En un script de despliegue basta con comprobar el código de salida de cosign verify (0 solo si la firma es válida).
Paso 6: Firmar sin claves desde GitHub Actions
Con la firma keyless no hay clave privada que custodiar. El runner de GitHub obtiene un token OIDC, Fulcio (la autoridad de certificación de Sigstore) emite un certificado de pocos minutos ligado a la identidad del workflow y la firma queda registrada en Rekor. Al verificar, compruebas quién firmó en lugar de qué clave.
En tu repositorio, crea .github/workflows/build.yml:
name: build-and-sign
on:
push:
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: build
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/your_user/demo-app:${{ github.ref_name }}
- uses: sigstore/cosign-installer@v3
- name: Firmar la imagen
run: cosign sign --yes "ghcr.io/your_user/demo-app@${DIGEST}"
env:
DIGEST: ${{ steps.build.outputs.digest }}
id-token: write es el permiso que permite al job pedir el token OIDC. No hace falta ningún secreto adicional. Publica un tag para lanzar el workflow:
git tag v1.0.1
git push origin v1.0.1
Cuando termine, verifica la imagen indicando la identidad esperada (el workflow y la referencia) y el emisor OIDC de GitHub:
cosign verify \
--certificate-identity-regexp '^https://github.com/your_user/demo-app/\.github/workflows/build\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/your_user/demo-app:v1.0.1
Verification for ghcr.io/your_user/demo-app:v1.0.1 --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The code-signing certificate was verified using trusted certificate authority certificates
Con esta verificación, solo se aceptan imágenes firmadas por ese workflow al ejecutarse sobre un tag v*. Una imagen firmada desde un fork, otra rama u otro repositorio no pasa.
Firmar con clave propia en CI
Si prefieres claves propias en el pipeline, guarda el contenido de cosign.key en un secreto COSIGN_PRIVATE_KEY y la contraseña en COSIGN_PASSWORD. Cosign puede leer la clave directamente de una variable de entorno, sin escribirla en disco:
- name: Firmar la imagen con clave
run: cosign sign --yes --key env://COSIGN_PRIVATE_KEY "ghcr.io/your_user/demo-app@${DIGEST}"
env:
DIGEST: ${{ steps.build.outputs.digest }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
Solución de problemas
no signatures found. La imagen no está firmada, el digest ha cambiado desde la firma o estás verificando en un registro distinto (por ejemplo, una copia en otro registro que no incluye la firma). Comprueba el digest actual con docker buildx imagetools inspect ghcr.io/your_user/demo-app:v1.0.0.
UNAUTHORIZED o denied al firmar. Cosign usa las credenciales de Docker. Repite docker login ghcr.io y confirma que el token tiene write:packages.
none of the expected identities matched en keyless. La identidad del certificado no coincide con --certificate-identity-regexp. El mensaje de error muestra la identidad real; ajusta la expresión (nombre del archivo del workflow, rama o tag).
error getting credentials en GitHub Actions. Falta permissions: id-token: write en el job.
Conclusión
Has instalado Cosign, has firmado y verificado una imagen con claves propias y has automatizado la firma keyless ligada a la identidad de un workflow de GitHub Actions. El siguiente paso lógico es exigir la firma en el propio clúster: con Kyverno puedes rechazar cualquier Pod cuya imagen no esté firmada con tu clave o tu workflow. También puedes adjuntar un SBOM firmado con cosign attest y firmar los artefactos que no son imágenes (binarios, charts) con cosign sign-blob.
