Firma de Imágenes de Contenedor con Cosign
Cosign es una herramienta de código abierto del proyecto Sigstore que permite firmar y verificar imágenes de contenedor para garantizar la integridad de la cadena de suministro de software, asegurando que las imágenes desplegadas en producción son exactamente las que fueron construidas y aprobadas. Soporta tanto la firma con claves propias como la firma sin clave (keyless) usando identidades efímeras verificadas por Fulcio y el registro de transparencia Rekor. Esta guía cubre el uso práctico de Cosign en pipelines CI/CD y Kubernetes.
Requisitos Previos
- Docker o acceso a un registro de contenedores (Docker Hub, GHCR, ECR, etc.)
- Cuenta en un registro OCI compatible con Cosign
- Para firma keyless: cuenta en un proveedor OIDC (GitHub, Google, Microsoft)
- Para verificación en Kubernetes: Kyverno o Connaisseur instalado
Instalación de Cosign
# Descargar Cosign desde el repositorio oficial de Sigstore
COSIGN_VERSION=$(curl -s https://api.github.com/repos/sigstore/cosign/releases/latest | \
grep '"tag_name"' | cut -d'"' -f4)
# Linux amd64
wget "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64"
install -m 755 cosign-linux-amd64 /usr/local/bin/cosign
# Verificar la instalación
cosign version
# macOS (con Homebrew)
# brew install cosign
# Verificar la autenticidad de Cosign con la clave pública de Sigstore
# (comprobación de seguridad del propio instalador)
cosign verify-blob --key https://raw.githubusercontent.com/sigstore/cosign/main/release/release-cosign.pub \
--signature cosign-linux-amd64.sig \
cosign-linux-amd64
Generación de Claves de Firma
# Generar un par de claves para firma de imágenes
cosign generate-key-pair
# Esto crea dos archivos:
# cosign.key - Clave privada (PROTEGER: no subir a repositorios)
# cosign.pub - Clave pública (puede ser pública)
# La clave privada se protege con una contraseña interactiva
# En CI/CD, usar la variable de entorno COSIGN_PASSWORD
# Generar claves protegidas con passphrase desde variable de entorno
COSIGN_PASSWORD="passphrase_segura" cosign generate-key-pair
# Guardar la clave privada como secreto en el sistema de CI/CD
# y la clave pública en el repositorio para verificación
# Alternativa: usar claves almacenadas en Vault/KMS
cosign generate-key-pair --kms gcr://proyectos/mi-proyecto/locations/global/keyRings/cosign/cryptoKeys/firma
cosign generate-key-pair --kms hashivault://cosign-signing-key
cosign generate-key-pair --kms awskms:///alias/cosign-key
Firma de Imágenes
Cosign guarda la firma como artefacto OCI en el mismo registro que la imagen:
# Primero, construir y publicar la imagen en el registro
docker build -t registry.empresa.com/mi-app:v1.2.3 .
docker push registry.empresa.com/mi-app:v1.2.3
# IMPORTANTE: Firmar por digest, no por tag (los tags son mutables)
# Obtener el digest SHA256 de la imagen
IMAGE_DIGEST=$(docker inspect registry.empresa.com/mi-app:v1.2.3 | \
python3 -c "import sys,json; print(json.load(sys.stdin)[0]['RepoDigests'][0].split('@')[1])")
echo "Digest: $IMAGE_DIGEST"
# Firmar la imagen usando la clave privada
COSIGN_PASSWORD="passphrase_segura" cosign sign \
--key cosign.key \
registry.empresa.com/mi-app@${IMAGE_DIGEST}
# También se puede firmar por tag (menos seguro, pero más cómodo)
COSIGN_PASSWORD="passphrase_segura" cosign sign \
--key cosign.key \
registry.empresa.com/mi-app:v1.2.3
# Añadir anotaciones a la firma (metadatos adicionales)
COSIGN_PASSWORD="passphrase_segura" cosign sign \
--key cosign.key \
--annotations "git-commit=$(git rev-parse HEAD)" \
--annotations "build-date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--annotations "[email protected]" \
registry.empresa.com/mi-app:v1.2.3
# Ver la firma almacenada en el registro
cosign triangulate registry.empresa.com/mi-app:v1.2.3
# Muestra la referencia OCI donde está guardada la firma
Verificación de Firmas
# Verificar la firma de una imagen con la clave pública
cosign verify \
--key cosign.pub \
registry.empresa.com/mi-app:v1.2.3
# La verificación devuelve la información del payload firmado en JSON
cosign verify \
--key cosign.pub \
registry.empresa.com/mi-app:v1.2.3 | python3 -m json.tool
# Verificar con anotaciones específicas (asegurar que coinciden los metadatos)
cosign verify \
--key cosign.pub \
--annotations "[email protected]" \
registry.empresa.com/mi-app:v1.2.3
# Script de verificación para uso en scripts de despliegue
cat > /usr/local/bin/verify-image.sh << 'SCRIPT'
#!/bin/bash
IMAGE="${1:?Especificar imagen a verificar}"
COSIGN_KEY="${COSIGN_PUBLIC_KEY_PATH:-/etc/cosign/cosign.pub}"
echo "Verificando firma de: $IMAGE"
if cosign verify --key "$COSIGN_KEY" "$IMAGE" > /dev/null 2>&1; then
echo "✓ Firma válida: $IMAGE"
exit 0
else
echo "✗ ERROR: Imagen sin firma válida o firma inválida: $IMAGE"
exit 1
fi
SCRIPT
chmod +x /usr/local/bin/verify-image.sh
Firma Keyless con Sigstore
La firma keyless usa identidades OIDC efímeras verificadas por Fulcio (CA de Sigstore):
# Firma keyless en un entorno interactivo (abre el navegador para autenticación)
cosign sign registry.empresa.com/mi-app:v1.2.3
# Firma keyless en GitHub Actions (usa la identidad OIDC del runner de GitHub)
# La variable COSIGN_EXPERIMENTAL=1 activa el modo keyless
COSIGN_EXPERIMENTAL=1 cosign sign registry.empresa.com/mi-app:v1.2.3
# Verificación de firma keyless (verificar la identidad que firmó)
cosign verify \
--certificate-identity "https://github.com/empresa/repositorio/.github/workflows/build.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
registry.empresa.com/mi-app:v1.2.3
# La verificación keyless consulta el registro de transparencia Rekor
# para confirmar que la firma existe y es válida
Integración en CI/CD
Pipeline GitHub Actions completo con Cosign:
# .github/workflows/build-and-sign.yml
name: Construir, publicar y firmar imagen
on:
push:
tags: ['v*']
jobs:
build-sign:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # Necesario para firma keyless OIDC
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configurar Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login en GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Construir y publicar imagen
id: build-push
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
# Siempre usar digest para firma segura
- name: Instalar Cosign
uses: sigstore/cosign-installer@main
- name: Firmar imagen (keyless, usando OIDC de GitHub Actions)
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}@${{ steps.build-push.outputs.digest }}
env:
COSIGN_EXPERIMENTAL: "1"
Script para firma con clave propia en CI/CD:
# En el pipeline CI/CD (con secretos configurados):
# CI_COSIGN_KEY: contenido de cosign.key (en base64)
# CI_COSIGN_PASSWORD: passphrase de la clave privada
cat > /usr/local/bin/sign-image-ci.sh << 'SCRIPT'
#!/bin/bash
IMAGE="${1:?Especificar imagen}"
# Recuperar la clave privada del entorno CI
echo "${CI_COSIGN_KEY}" | base64 -d > /tmp/cosign.key
chmod 600 /tmp/cosign.key
# Firmar la imagen
COSIGN_PASSWORD="${CI_COSIGN_PASSWORD}" cosign sign \
--key /tmp/cosign.key \
--annotations "ci-run=${CI_RUN_ID:-local}" \
--annotations "git-sha=${GIT_SHA:-$(git rev-parse HEAD)}" \
"$IMAGE"
EXIT_CODE=$?
rm -f /tmp/cosign.key
exit $EXIT_CODE
SCRIPT
chmod +x /usr/local/bin/sign-image-ci.sh
Políticas de Verificación en Kubernetes
Usar Kyverno para verificar automáticamente las firmas al desplegar:
# Crear política Kyverno que requiere firma Cosign en todas las imágenes
cat > /tmp/kyverno-verify-images.yaml << 'EOF'
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
annotations:
policies.kyverno.io/description: >-
Verifica que todas las imágenes de contenedor están firmadas con Cosign
usando la clave pública de la empresa antes de permitir su despliegue.
spec:
validationFailureAction: Enforce
background: false
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds: [Pod]
namespaces: ["produccion", "staging"]
verifyImages:
- imageReferences:
- "registry.empresa.com/*"
- "ghcr.io/empresa/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE... (tu clave pública aquí)
-----END PUBLIC KEY-----
EOF
kubectl apply -f /tmp/kyverno-verify-images.yaml
# Probar que la política funciona
kubectl run test \
--image=registry.empresa.com/mi-app:v1.2.3 \
--namespace=produccion
# Si la imagen está firmada: se crea el pod
# Si la imagen no está firmada: se rechaza con error
Solución de Problemas
# Error: "no signatures found"
# La imagen no está firmada o la firma está en un registro diferente
cosign verify --key cosign.pub registry.empresa.com/mi-app:v1.2.3 2>&1
# Verificar si la firma existe en el registro
cosign triangulate registry.empresa.com/mi-app:v1.2.3
# Error de autenticación con el registro
# Asegurarse de estar autenticado con docker login
docker login registry.empresa.com
cosign sign --key cosign.key registry.empresa.com/mi-app:v1.2.3
# Error: "MANIFEST_UNKNOWN" al firmar una imagen que no existe
# Verificar que la imagen se publicó correctamente antes de firmar
docker pull registry.empresa.com/mi-app:v1.2.3
# Listar todas las firmas y atestaciones de una imagen
cosign tree registry.empresa.com/mi-app:v1.2.3
# Ver el payload de la firma en detalle
cosign verify --key cosign.pub \
--output json \
registry.empresa.com/mi-app:v1.2.3 2>/dev/null | python3 -m json.tool
Conclusión
Cosign resuelve el problema de integridad de la cadena de suministro de software para contenedores de forma práctica y sin necesidad de infraestructura compleja adicional, almacenando las firmas directamente en el registro OCI existente. La combinación de Cosign para la firma en CI/CD con Kyverno o Gatekeeper para la verificación en el momento del despliegue en Kubernetes garantiza que solo las imágenes autorizadas y verificadas se ejecutan en producción, cerrando uno de los vectores de ataque más críticos en las cadenas de suministro modernas.


