El Modelo de Amenazas Que Nadie Dibuja
La mayoría de los equipos piensan en ataques a la cadena de suministro como "alguien envenena una dependencia." Ese es un vector, pero la superficie de ataque completa es mucho más amplia:
Mapa de Superficie de Ataque:
┌─────────────────────────────────────────────────┐
│ 1. Código Fuente │
│ - Cuenta de developer comprometida │
│ - PR malicioso desde fork (typosquatting CI) │
│ - Commits sin firmar (impersonación) │
│ │
│ 2. Dependencias │
│ - Paquetes typosquatting │
│ - Cuenta de maintainer comprometida │
│ - Confusión de dependencias (privado/público)│
│ - Scripts postinstall maliciose es │
│ │
│ 3. Pipeline de Build │
│ - Archivos de CI workflow modificados │
│ - Entorno de build comprometido │
│ - Pase es de build inyectados │
│ - Secrets exfiltrados durante el build │
│ │
│ 4. Almacenamiento de Artefactos │
│ - Artefactos reemplazados en el registry │
│ - Credenciales del registry comprometidas │
│ - Man-in-the-middle en descarga de artefactos│
│ │
│ 5. Deployment │
│ - Scripts de deploy modificados │
│ - Credenciales de deploy comprometidas │
│ - Inyección de dependencias en runtime │
└─────────────────────────────────────────────────┘
SLSA, Sigstore, y SBOMs cada uno aborda diferentes partes de esta superficie. Las siguientes secciones recorren cada uno con detalles de implementación reales.
Sigstore: Firma Keyless Que Realmente Funciona
El modelo viejo de firma de código requería manejar claves de larga vida, lo cual creaba sus propios problemas de seguridad (almacenamiento, rotación, revocación de claves). Sigstore reemplaza esto con claves efímeras atadas a identidad, usando los mismos tokens OIDC que tu CI ya tiene.
Así funciona la firma keyless por debajo:
# 1. Instalar cosign
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# 2. Firmar una imagen de container (keyless, en CI)
# El flag --yes salta la confirmación interactiva
cosign sign --yes ghcr.io/myorg/myapp:v1.2.3
# Qué pasa detrás de escena:
# a) cosign pide un token OIDC al proveedor de CI
# (GitHub Actions: ACTIONS_ID_TOKEN_REQUEST_URL)
# b) Manda el token OIDC al CA Fulcio de Sigstore
# c) Fulcio verifica el token, emite un certificado de vida corta
# (vinculado a la identidad del workflow, válido ~10 minutos)
# d) cosign genera un par de claves efímero
# e) Firma el digest de la imagen con la clave privada efímera
# f) Registra la firma + certificado en el log de transparencia Rekor
# g) Descarta la clave privada (nunca se almacena en ningún lado)
La verificación es donde se pone poderoso:
# Verificar una imagen firmada, chequeando claims de identidad
cosign verify \
--certificate-identity "https://github.com/myorg/myapp/.github/workflows/release.yml@refs/tags/v1.2.3" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/myorg/myapp:v1.2.3
# Esto chequea:
# 1. La firma es válida
# 2. El certificado de firma fue emitido por Fulcio
# 3. La identidad del certificado matchea el workflow esperado
# 4. El evento de firma existe en el log de transparencia Rekor
# 5. El certificado era válido al momento de la firma
Este es un workflow de GitHub Actions para firmar imágenes de container:
name: Build and Sign
on:
push:
tags: ['v*']
permissions:
contents: read
packages: write
id-token: write # Requerido para firma keyless
jobs:
build-sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- name: Login a GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build y push
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
- name: Firmar imagen
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
- name: Adjuntar SBOM
run: |
syft ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} \
-o cyclonedx-json > sbom.cdx.json
cosign attach sbom \
--sbom sbom.cdx.json \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
SLSA: Provenance Que Significa Algo
SLSA (se pronuncia "salsa") te da un framework para pensar sobre integridad de builds. El concepto clave es provenance, un registro verificable de cómo se construyó un artefacto, incluyendo:
- Qué código fuente se usó
- Qué sistema de build lo produjo
- Cuáles fueron los pase es de build
- Cuáles fueron los inputs y outputs
El generador SLSA para GitHub Actions lo hace casi llave en mano:
# .github/workflows/slsa-build.yml
name: SLSA Build
on:
push:
tags: ['v*']
permissions:
contents: read
id-token: write
actions: read
jobs:
build:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.hash.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Build
run: npm ci && npm run build
- name: Empaquetar
run: tar -czf dist.tar.gz dist/
- name: Generar hash
id: hash
run: |
DIGEST=$(sha256sum dist.tar.gz | cut -d' ' -f1)
echo "digest=sha256:${DIGEST}" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with:
name: build-artifact
path: dist.tar.gz
provenance:
needs: build
permissions:
actions: read
id-token: write
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0
with:
base64-subjects: |
${{ needs.build.outputs.digest }}
El documento de provenance se ve así (simplificado):
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{
"name": "dist.tar.gz",
"digest": { "sha256": "a1b2c3d4..." }
}],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://github.com/slsa-framework/slsa-github-generator/generic@v1",
"externalParameters": {
"source": {
"uri": "git+https://github.com/myorg/myapp@refs/tags/v1.2.3",
"digest": { "sha1": "abc123..." }
}
}
},
"runDetails": {
"builder": {
"id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v1.10.0"
},
"metadata": {
"invocationId": "https://github.com/myorg/myapp/actions/runs/12345678"
}
}
}
}
SBOMs: Sabé Qué Estás Despachando
Un SBOM (Software Bill of Materials) es un inventario de cada componente en tu software. Hay dos formatos dominantes: CycloneDX y SPDX. CycloneDX tiende a ser la opción preferida porque su tooling es más maduro para dependencias de aplicaciones.
# Generar SBOM desde una imagen de container
syft ghcr.io/myorg/myapp:v1.2.3 -o cyclonedx-json > sbom.cdx.json
# Generar SBOM desde source (proyecto npm)
cdxgen -t node -o sbom.cdx.json .
# Generar SBOM incluyendo paquetes del SO en container
syft ghcr.io/myorg/myapp:v1.2.3 \
--scope all-layers \
-o cyclonedx-json > sbom-full.cdx.json
La diferencia entre un lockfile y un SBOM es importante. En un proyecto representativo de Node.js:
package-lock.json: 847 dependencias
SBOM (solo source): 847 dependencias (igual al lockfile)
SBOM (container): 1.243 dependencias (incluye libc, openssl,
zlib, libcurl, ca-certificates,
runtime de node, etc.)
Esas 396 dependencias extra son superficie de ataque real. Cuando sale una vulnerabilidad en libwebp o openssl, tu lockfile no te dice nada, pero tu SBOM de container sí.
npm Provenance: Un Flag Para Gobernarlos a Todos
Desde npm 9.5, es posible publicar paquetes con provenance criptográfico en un solo paso:
npm publish --provenance
Eso es todo. Detrás de escena, esto:
- Genera un predicado de provenance SLSA vinculando el paquete al run de GitHub Actions
- Lo firma con firma keyless de Sigstore
- Sube el provenance firmado a npm junto con el paquete
- npm muestra un badge "published with provenance"
Tu package.json no necesita nada especial. El único requisito es publicar desde un workflow de GitHub Actions con permiso id-token: write.
# .github/workflows/publish.yml
name: Publish
on:
release:
types: [published]
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Los consumidores pueden verificar provenance:
npm audit signatures
# Chequea que todos los paquetes instalados tengan firmas Sigstore válidas
Lo Que Realmente Importa: Priorización Pragmática
Basado en experiencia de implementación en múltiples organizaciones, este es un ranking pragmático de qué entrega más valor de seguridad por hora de esfuerzo de ingeniería:
- npm provenance (30 min de setup, valor masivo), Un flag, attestation criptográfica. Hacer esto primero.
- Firma de imágenes de container con cosign (2 horas), Firma keyless en CI, verificación en deploy.
- Generación y escaneo de SBOMs (4 horas), Conocer las dependencias, atrapá vulns en todo el stack.
- SLSA Level 2 provenance (1 día), Provenance autenticado desde un servicio de build hosted.
- SLSA Level 3 (1-2 semanas), Entornos de build endurecidos, aislados. Acá es donde se pone caro en tiempo de ingeniería.
- Builds reproducibles (esfuerzo continuo), El santo grial. Prácticamente difícil para la mayoría de los proyectos.
En un caso documentado, después de implementar los items 1-4, el tiempo de detección para un compromiso similar del pipeline bajó de 11 horas a unos 4 minutos, porque el mismatch de identidad de firma se atraparía en el momento del deploy.
La seguridad de la cadena de suministro no se trata de alcanzar algún nivel SLSA perfecto. Se trata de hacer caro atacar un pipeline y barato detectar cuando alguien lo intenta. Comenzar con la fruta al alcance de la mano, medir la brecha de detección, y subir desde ahí.