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.