El tercer frente pendiente se formuló en el módulo 3 con una frase incómoda: "el pipeline tiene permisos sobre producción y no se ha auditado". Es literalmente cierto. cd.yml puede desplegar cualquier imagen en prod, infra.yml puede modificar la infraestructura de AWS, y ambos ejecutan acciones de terceros que nadie del equipo ha leído. Si alguien compromete el repositorio de Reservalia no necesita atacar producción: el pipeline la despliega por él. Y la lección anterior añadió el otro lado del mismo problema: 1.147 paquetes de unos 330 mantenedores se descargan, se ejecutan y acaban dentro de la imagen. Esta lección aborda las dos mitades —la seguridad del software que pasa por el pipeline y la seguridad del pipeline mismo— y termina con la cadena de suministro: SBOM, firma y procedencia. Con una advertencia previa que conviene tomarse en serio: esto enseña a automatizar controles, no sustituye una revisión por un profesional de seguridad ni el criterio de compliance de tu organización. Un pipeline con seis escáneres en verde no es un sistema seguro: es un sistema con seis escáneres en verde.

Contenido

  1. El pipeline como superficie de ataque
  2. Los cinco análisis automatizados y qué detecta cada uno
  3. El job seguridad de Reservalia
  4. La política de severidades: qué rompe el build y qué abre un ticket
  5. Secretos: dónde viven y por qué OIDC era una decisión de seguridad
  6. Cuando un secreto se filtra
  7. Mínimo privilegio en los permisos del pipeline
  8. Código de terceros: fijar acciones por SHA y el riesgo de pull_request_target
  9. Protección de ramas, entornos y registro de auditoría
  10. Cadena de suministro: SBOM, firma y procedencia
  11. Errores Comunes y Consejos
  12. Ejercicios
  13. Conclusión

  1. El pipeline como superficie de ataque

Durante dos módulos hemos tratado el pipeline como una herramienta de productividad. Visto desde fuera es otra cosa: un sistema con acceso privilegiado que ejecuta código de terceros y avala con su reputación lo que produce.

Propiedad del pipeline Por qué le interesa a un atacante
Tiene credenciales de prod Es un camino a producción que no pasa por ningún cortafuegos
Ejecuta código de terceros Acciones, paquetes npm y sus scripts de instalación corren con esos permisos
Publica los artefactos de confianza Lo que sale de él nadie lo vuelve a cuestionar
Reacciona a eventos de desconocidos Un pull request de un fork puede disparar trabajo
Sus logs pasan por muchos ojos Un secreto impreso una vez queda en el historial

De ahí las dos preguntas que estructuran la lección. La primera es ¿el software que entregamos tiene fallos de seguridad?, y se responde metiendo análisis dentro del pipeline: es el shift-left, adelantar la detección al momento más barato, el mismo argumento del fail fast de la 04-01 aplicado a otra clase de defecto. La segunda es ¿el pipeline mismo es seguro?, y se responde con permisos, secretos y control del código de terceros. Un equipo que solo hace lo primero tiene escáneres bonitos protegiendo un sistema que cualquiera puede secuestrar.

  1. Los cinco análisis automatizados y qué detecta cada uno

Tipo Qué analiza Qué detecta Cuándo se ejecuta Falsos positivos
SAST Tu código fuente Inyección SQL, XSS, rutas inseguras, criptografía mal usada En cada PR Medios
SCA Tus dependencias Versiones con vulnerabilidades conocidas (CVE) En cada PR y a diario Bajos, pero mucho ruido irrelevante
Detección de secretos Código e historial de git Claves, tokens y contraseñas comprometidos Cada PR, y una vez sobre todo el historial Bajos
Escaneo de imagen La imagen construida Vulnerabilidades del sistema base y de las capas Tras docker build Altos: muchas no son explotables
DAST La aplicación en ejecución Cabeceras, autenticación, configuración expuesta Nocturno, contra staging Medios

Las diferencias importan más que los nombres. SAST lee código sin ejecutarlo: encuentra el patrón peligroso pero no sabe si esa ruta es alcanzable, y de ahí sus falsos positivos. SCA no mira tu código en absoluto, solo compara tu árbol de dependencias con una base de datos de vulnerabilidades; es el más barato y el que más ruido genera, porque una CVE en una librería que solo usas en pruebas no es un riesgo real. La detección de secretos es la única que debe mirar el historial completo: borrar una clave en un commit posterior no la elimina de git, sigue estando a un git log -p de distancia. El escaneo de imagen encuentra decenas de vulnerabilidades del sistema base que ni siquiera tienen parche disponible. Y DAST es el único que prueba el sistema real, con su configuración y sus cabeceras; por eso va contra staging y en el pipeline nocturno: es lento y necesita algo desplegado.

Ninguno sustituye a los demás, y ninguno sustituye a pensar. Una vulnerabilidad de lógica de negocio —"puedo cancelar la cita de otro negocio cambiando el identificador de la URL"— no la detecta ninguna de las cinco. Eso se cubre con diseño, revisión y pruebas escritas a propósito.

  1. El job seguridad de Reservalia

Reservalia añade un quinto job a ci.yml, en paralelo con los otros para no alargar el camino crítico:

  seguridad:                                   # ~2 min · en paralelo con calidad/test/build
    name: Seguridad
    runs-on: ubuntu-22.04
    timeout-minutes: 15
    permissions:
      contents: read
      security-events: write                   # 1 · subir resultados a la pestaña Security
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }               # 2 · gitleaks necesita el historial

      - name: Secretos filtrados
        uses: gitleaks/gitleaks-action@v2      # 3
        env: { GITLEAKS_CONFIG: .gitleaks.toml }

      - uses: github/codeql-action/init@v3     # 4 · SAST
        with: { languages: javascript-typescript, config-file: .github/codeql/config.yml }
      - uses: github/codeql-action/analyze@v3

      - name: Dependencias vulnerables (SCA)
        run: npm audit --audit-level=high --omit=dev    # 5

      - name: Escanear la imagen
        uses: aquasecurity/[email protected]          # 6
        with:
          image-ref: reservalia/api:${{ github.sha }}
          severity: 'CRITICAL,HIGH'
          ignore-unfixed: true                          # 7
          exit-code: '1'
          trivyignores: .trivyignore
  1. security-events: write permite que los hallazgos aparezcan en la pestaña Security del repositorio en lugar de perderse en un log. Un hallazgo que solo existe en la salida de un job es un hallazgo que nadie leerá dos veces.
  2. fetch-depth: 0 trae el historial completo. Por defecto el checkout es superficial y gitleaks solo vería el último commit, con lo que un secreto comprometido hace ocho meses pasaría desapercibido.
  3. gitleaks busca patrones de credenciales. En .gitleaks.toml se declaran las excepciones justificadas —una clave de ejemplo en la documentación, un token de un fixture— con un comentario que explique por qué no son reales.
  4. CodeQL es el SAST de GitHub. Analiza el flujo de datos entre las entradas del programa y las operaciones peligrosas, así que detecta que un parámetro de req.query acaba concatenado en una consulta SQL aunque pase por tres funciones intermedias.
  5. npm audit con --audit-level=high solo falla ante lo grave, y --omit=dev excluye las herramientas de desarrollo: una denegación de servicio en el ejecutor de pruebas no es un riesgo de producción y, si bloquea el merge, el equipo aprenderá a saltarse la comprobación.
  6. Trivy analiza la imagen que acaba de construir el job build: paquetes del sistema base, librerías del intérprete y dependencias empaquetadas. Es un escaneo distinto del de npm audit porque cubre lo que hay debajo de tu código.
  7. ignore-unfixed: true es el ajuste que hace la herramienta usable: oculta las vulnerabilidades sin parche disponible. Sin él, la primera ejecución devuelve decenas de hallazgos sobre los que no se puede hacer nada, y la reacción natural del equipo es dejar de mirar.

La primera ejecución sobre Reservalia da el resultado habitual: 1 secreto en el historial —una DATABASE_URL de staging comprometida en 2024 y "borrada" al día siguiente—, 3 hallazgos altos de CodeQL, 9 vulnerabilidades de dependencias de las que 2 son altas, y 31 de la imagen base, 26 de ellas sin parche. Es un cuadro perfectamente normal, y la reacción correcta no es bloquear el pipeline hasta arreglarlo todo, sino la del apartado siguiente.

  1. La política de severidades: qué rompe el build y qué abre un ticket

Aquí es donde la mayoría de equipos fracasan, siempre del mismo modo: se activa todo en bloqueante, el pipeline se pone rojo por cosas que nadie puede arreglar hoy, alguien añade continue-on-error: true "temporalmente", y seis meses después el job de seguridad lleva medio año en amarillo sin que nadie lo mire. Una alerta que se ignora es peor que no tenerla, porque además da sensación de cobertura.

Severidad Efecto Plazo Quién
Secreto detectado Bloquea siempre, sin excepción Inmediato: rotar la credencial Nuria
Crítica con parche disponible Bloquea el merge Inmediato El autor del PR
Alta con parche, en código nuevo Bloquea el merge Inmediato El autor del PR
Alta heredada (ya estaba en main) Informa y abre ticket 30 días Turno de dependencias
Media Informa 90 días o al tocar esa zona Turno de dependencias
Baja o sin parche disponible Se registra, no notifica Revisión trimestral Marta

Tres principios sostienen la tabla. La distinción entre nuevo y heredado es la misma del quality gate de SonarQube de la 02-05: no puedes exigir a un PR de veinte líneas que resuelva los 1.847 avisos anteriores, pero sí que no añada uno nuevo; con eso la deuda deja de crecer y se reduce sola conforme se toca el código. Los secretos son la única categoría sin matices: una credencial comprometida lo está aunque el commit se borre, así que no hay "lo arreglo la semana que viene". Y cada severidad tiene dueño y plazo: un hallazgo sin dueño no es una tarea, es un aviso.

Un matiz sobre las excepciones, porque las va a haber. Cuando una vulnerabilidad no aplica —la ruta no es alcanzable, la librería solo se usa en un script de desarrollo—, se documenta con motivo y fecha de caducidad:

# .trivyignore
# CVE-2024-XXXXX · openssl del sistema base. No alcanzable: la API no abre
# conexiones TLS salientes. Revisar el 2026-10-01 o al cambiar de imagen base.
CVE-2024-XXXXX

Una exclusión sin fecha ni motivo es indistinguible de un descuido, y a los dos años nadie sabe si sigue siendo cierta.

  1. Secretos: dónde viven y por qué OIDC era una decisión de seguridad

En la 03-02 elegimos OIDC y Secrets Manager por comodidad operativa. Ahora toca justificarlo como lo que era.

Dónde Quién puede leerlo Rotación Qué pasa si se filtra
Variable de entorno del repositorio Cualquiera que pueda ejecutar un workflow Manual Vale hasta que alguien la cambie
Secreto de repositorio en GitHub Cualquier workflow del repositorio Manual Igual, pero enmascarado en los logs
Secreto de entorno (prod) Solo jobs con environment: prod Manual Limitado a ese entorno
OIDC (sin secreto) Solo quien cumpla la condición del rol No aplica: no hay secreto Nada que filtrar
Secrets Manager Solo la tarea en ejecución Sin desplegar Queda auditado en CloudTrail

La fila que más sorprende es la primera. Un secreto de repositorio sin ámbito de entorno está disponible para cualquier workflow del repositorio, incluido uno que alguien añada en un pull request: basta un workflow de tres líneas que imprima el valor codificado en base64 —el enmascarado de GitHub oculta el literal, no una transformación— para llevárselo. Por eso los secretos de producción de Reservalia viven solo en el entorno prod, que además exige aprobación humana.

Y por eso OIDC es cualitativamente distinto: no es "un secreto mejor guardado", es la ausencia de secreto. GitHub emite un token de vida corta que afirma quién es el workflow, y AWS decide si esa afirmación encaja con la condición del rol. No hay nada que rotar, nada que filtrar y nada que caduque mal. La regla que se deriva: prefiere identidad federada a credenciales; si no es posible, secretos de ámbito estrecho; y si tampoco, al menos rota con calendario. Sobre la rotación: un secreto que no se rota nunca tiene exposición acumulada total —pasó por todas las máquinas y todas las personas que tocaron el sistema desde que existe—. Reservalia rota trimestralmente lo que queda (el token de la pasarela de pagos, la clave del proveedor de SMS) y ensaya la rotación igual que ensaya el rollback, porque una rotación que nadie ha probado falla justo el día que hay que hacerla deprisa.

  1. Cuando un secreto se filtra

Este apartado importa porque el orden de los pasos determina el daño, y el error más común es empezar por el paso equivocado.

  1. Rotar la credencial. Primero, y antes que nada. Generar una nueva, desplegarla, invalidar la vieja. Mientras la credencial siga siendo válida, todo lo demás es cosmético.
  2. Revisar qué se hizo con ella. CloudTrail, logs del proveedor, accesos anómalos. Asumir que se usó, no que no.
  3. Después, limpiar el historial de git si procede. Es lo más laborioso —reescribir el historial rompe los clones de todo el mundo— y lo menos urgente, porque el daño ya está hecho.
  4. Anotarlo como incidente, con causa y medida preventiva.

El error clásico es hacer el 3 primero: borrar el commit, respirar aliviado y no rotar. Da la sensación de haber resuelto el problema mientras la clave sigue funcionando y probablemente ya está en el clon de alguien o en un índice automático. La regla, sin excepciones: un secreto que ha estado en un repositorio está comprometido, aunque el repositorio sea privado y aunque el commit se haya borrado. En Reservalia, la DATABASE_URL de staging del apartado 3 se trata así: se rota la contraseña de RDS ese mismo día, se revisa CloudTrail buscando conexiones desde fuera de la VPC, y se decide no reescribir el historial —era staging, la base no era accesible desde internet, y reescribir dos años de historia tiene su propio coste—, dejándolo documentado en SECURITY.md. La decisión razonada y escrita es parte de la respuesta.

  1. Mínimo privilegio en los permisos del pipeline

Todo job debería tener exactamente los permisos que necesita y ni uno más, en dos planos: los del token de GitHub y los del rol de AWS.

# .github/workflows/ci.yml — cabecera
permissions:
  contents: read              # 1 · valor por defecto para TODO el workflow

jobs:
  publicar:
    permissions:
      contents: read
      id-token: write         # 2 · solo este job puede pedir el token OIDC
  1. Declarar permissions a nivel de workflow es lo primero que hay que hacer en un repositorio existente. El valor por defecto de una organización antigua puede ser write-all, lo que significa que un script comprometido en cualquier job puede empujar a main, cerrar issues o publicar releases.
  2. id-token: write solo donde hace falta, porque es el permiso que permite pedir el token OIDC y, con él, asumir roles de AWS. Un job de lint no lo necesita.

En el lado de AWS rige el mismo principio sobre los roles ya creados: AWS_ROLE_CI solo necesita empujar a un repositorio de ECR —no leer secretos, no tocar ECS—; reservalia-deploy-prod necesita actualizar el servicio de prod y nada de dev; y reservalia-terraform-plan es un rol de solo lectura, distinto de reservalia-terraform-apply, que es justamente lo que permite ejecutar el plan en cada pull request sin miedo.

La pregunta que ordena el apartado entero: si este job estuviera comprometido, ¿hasta dónde llegaría? Si la respuesta es "hasta producción" y el job es el que ejecuta las pruebas del PR de un desconocido, hay un problema de diseño y no de configuración. Esa es la razón profunda de que ci.yml y cd.yml sean dos ficheros: la frontera de privilegios de la 04-01.

  1. Código de terceros: fijar acciones por SHA y el riesgo de pull_request_target

uses: actions/checkout@v4 significa "ejecuta lo que sea que haya hoy en la etiqueta v4". Una etiqueta de git es móvil: quien controle el repositorio de la acción puede reapuntarla a otro commit, y tu pipeline ejecutará ese código con sus permisos, sin que cambie una sola línea de tu repositorio.

      # Frágil: la etiqueta se puede mover
      - uses: alguien/accion-util@v3

      # Robusto: el SHA es inmutable
      - uses: alguien/accion-util@8f4b7c2e1d09a6f35b2c0e7a91d4f8b6c3a2e5d1  # v3.2.1

El SHA es una huella criptográfica del contenido y no se puede reapuntar. El comentario con la versión legible es imprescindible para que el pull request de Dependabot que lo actualice signifique algo para un humano, y con la entrada github-actions del dependabot.yml de la 04-02 esos SHA se mantienen al día automáticamente. Reservalia aplica la regla por capas, que es lo pragmático: acciones de terceros, siempre por SHA; acciones oficiales de GitHub y de AWS, por etiqueta mayor, aceptando el riesgo de forma consciente a cambio de menos ruido.

pull_request_target merece párrafo propio porque es la trampa más peligrosa de GitHub Actions. El disparador normal, pull_request, ejecuta el workflow sin secretos cuando el PR viene de un fork: es el comportamiento seguro por defecto. pull_request_target existe para los casos en que un workflow necesita secretos al procesar PR externos —etiquetar, comentar—, y funciona ejecutando el workflow de la rama base pero con acceso completo a los secretos. El fallo aparece en cuanto alguien añade un checkout del código del PR:

on: pull_request_target          # ← con secretos disponibles
jobs:
  peligroso:
    steps:
      - uses: actions/checkout@v4
        with: { ref: '${{ github.event.pull_request.head.sha }}' }   # ← código del atacante
      - run: npm ci                                                   # ← lo ejecuta con secretos

Un npm ci ejecuta los scripts postinstall del package.json del PR, así que cualquiera puede abrir un pull request desde un fork con un postinstall que envíe todos los secretos del entorno a un servidor propio. Regla: no combines pull_request_target con un checkout del código del PR. Si necesitas ambas cosas, sepáralas en dos workflows: uno sin privilegios que construye y guarda un artefacto, y otro con privilegios que solo consume ese artefacto sin ejecutarlo. Reservalia no usa pull_request_target en ningún sitio, que es la respuesta más simple.

  1. Protección de ramas, entornos y registro de auditoría

Las reglas de la 02-07 —revisión obligatoria, checks en verde, CODEOWNERS, sin force-push, incluidos los administradores— dejan de ser higiene de proceso y pasan a ser controles de seguridad en cuanto se ve el pipeline como un camino a producción: si main se puede modificar sin revisión, todo lo demás sobra, porque quien modifique main modifica también el workflow que despliega. Tres controles adicionales que Reservalia añade ahora:

Control Qué impide Coste
Firmar los commits (GPG o SSH) Suplantar la autoría de un commit Una configuración por persona
CODEOWNERS sobre .github/ Que un cambio de pipeline se fusione sin que lo vea Nuria Ninguno: ya existía
Aprobación para PR de colaboradores externos Ejecutar workflows de desconocidos sin mirar Un clic por PR

Y una capacidad que se descubre tarde y vale mucho: el registro de auditoría. GitHub guarda quién cambió un secreto, quién aprobó un despliegue a prod, quién modificó una regla de protección y quién ejecutó rollback.yml; AWS guarda en CloudTrail cada llamada a la API con la identidad que la hizo, incluidos los AssumeRoleWithWebIdentity de OIDC. Merece la pena mirarlos antes de necesitarlos, por dos razones: para saber que existen y qué contienen, y para descubrir en frío cosas como que un rol que se creía eliminado se sigue usando cada noche.

  1. Cadena de suministro: SBOM, firma y procedencia

Las tres preguntas que un equipo debe poder responder sobre cualquier artefacto en producción son: qué contiene, quién lo produjo y cómo se produjo. Un mecanismo para cada una.

SBOM (Software Bill of Materials): la lista completa de componentes de un artefacto en un formato estándar (SPDX o CycloneDX). Responde a qué contiene, y su valor real se ve el día que aparece una vulnerabilidad grave en una librería: en lugar de investigar durante horas, se consulta el SBOM de cada versión desplegada y en dos minutos se sabe si afecta y desde cuándo.

# infra/scripts/generar-sbom.sh
syft "reservalia/api@${DIGEST}" -o spdx-json > "sbom-${SHA}.spdx.json"        # 1
cosign attach sbom --sbom "sbom-${SHA}.spdx.json" "reservalia/api@${DIGEST}"  # 2
  1. Syft inspecciona la imagen ya construida y lista todo: paquetes npm, paquetes del sistema base, versiones y licencias. Se genera sobre el artefacto, no sobre el código fuente, porque lo que importa es lo que se desplegó de verdad.
  2. Se adjunta al artefacto en el registro en lugar de guardarse en un cajón: así el SBOM viaja con la imagen y se recupera por digest años después.

Firma con cosign: responde a quién lo produjo. En modo keyless aprovecha el mismo OIDC del apartado 5, de modo que tampoco hay clave privada que custodiar y la identidad del firmante es el propio workflow.

      - uses: sigstore/cosign-installer@v3
      - run: |
          cosign sign --yes "${ECR}/reservalia/api@${DIGEST}"
          cosign verify --certificate-identity-regexp '.*reservalia/reservalia.*' \
            --certificate-oidc-issuer https://token.actions.githubusercontent.com \
            "${ECR}/reservalia/api@${DIGEST}"

Firmar sin verificar no sirve de nada: la verificación tiene que ocurrir antes de desplegar, dentro de cd.yml, para que una imagen que aparezca en ECR sin firma válida no llegue nunca a producción. Es el equivalente criptográfico del smoke test: comprobar que lo que vas a desplegar es lo que crees.

Atestaciones de procedencia: responden a cómo se produjo. Son un documento firmado que declara de qué commit, con qué workflow y en qué momento se construyó el artefacto. Con eso la cadena queda cerrada: del digest se llega al commit, del commit al pull request, y del pull request a quien lo revisó. SLSA es el marco que ordena todo esto en niveles y sirve como hoja de ruta:

Nivel Qué exige Reservalia
1 Build automatizada que genera procedencia ✅ desde el módulo 2
2 Servicio de build alojado y procedencia firmada ✅ al añadir cosign
3 Build aislada y no falsificable, fuente verificada ⚠️ requiere endurecer permisos y aislar
4 Revisión de dos personas y build hermética y reproducible ❌ objetivo lejano

Lo razonable para un equipo como el de Reservalia es alcanzar el nivel 2 y quedarse ahí de forma consciente, dejando el 3 documentado como objetivo. Subir de nivel cuesta, y un nivel alcanzado a medias que nadie verifica no aporta nada.

Errores Comunes y Consejos

Error 1: activar todos los escáneres en modo bloqueante el primer día. El pipeline se pone rojo por cosas sin solución, alguien añade continue-on-error y la seguridad desaparece del proceso sin que nadie lo decida. Error 2: no distinguir hallazgos nuevos de heredados, que es la forma de garantizar que la deuda nunca baje.

Error 3: borrar el commit del secreto y no rotar la credencial. El commit es lo de menos: la clave sigue siendo válida. Error 4: guardar secretos de producción como secretos de repositorio en lugar de secretos de entorno, con lo que quedan al alcance de cualquier workflow, incluido uno abierto en un PR.

Error 5: usar acciones de terceros por etiqueta. Una etiqueta se puede mover; un SHA no. Error 6: combinar pull_request_target con checkout del código del PR, que es entregar los secretos a cualquiera que sepa escribir un postinstall.

Error 7: permissions: write-all heredado de la organización, con lo que un job de lint puede empujar a main. Error 8: firmar artefactos y no verificar la firma antes de desplegar: una ceremonia sin efecto.

Consejo 1: empieza en modo informativo y bloquea por fases, categoría a categoría, cuando el ruido esté bajo control. Consejo 2: pon fecha y motivo a cada exclusión, o en un año nadie sabrá si sigue siendo válida. Consejo 3: escribe un SECURITY.md con a quién avisar, qué se hace ante un secreto filtrado y qué se espera de un colaborador externo. Consejo 4: pide una revisión profesional antes de cualquier certificación o compromiso contractual; este curso te prepara para esa conversación, no la sustituye.

Ejercicios

Ejercicio 1

Un equipo activa Trivy con la configuración por defecto. La primera ejecución devuelve 214 vulnerabilidades en la imagen: 3 críticas, 18 altas y el resto medias y bajas; 180 no tienen parche disponible. El pipeline queda bloqueado y nadie puede fusionar. Propón un plan concreto para las próximas cuatro semanas.

Ejercicio 2

Revisa este workflow y enumera todos los problemas de seguridad que encuentres, ordenados por gravedad, indicando la corrección de cada uno:

on: pull_request_target
permissions: write-all
jobs:
  validar:
    steps:
      - uses: actions/checkout@v4
        with: { ref: '${{ github.event.pull_request.head.sha }}' }
      - uses: cualquiera/setup-tool@main
      - run: npm ci && npm test
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET }}

Ejercicio 3

Aparece una vulnerabilidad crítica en una librería muy extendida. Marta pregunta: "¿estamos afectados, desde cuándo y qué versiones tenemos desplegadas?". Explica cómo se responde con lo construido en esta lección y en la anterior, y qué faltaría si Reservalia no hubiera generado SBOM.

Soluciones

Solución 1. El error de partida es haber activado en bloqueante una herramienta cuyo ruido no se conocía. Plan por semanas:

Semana 1 — dejar de bloquear sin dejar de mirar. Poner el escaneo en informativo (exit-code: '0'), activar ignore-unfixed: true y limitar a CRITICAL,HIGH. De 214 hallazgos se pasa a unos 20 accionables: los 180 sin parche salen del ruido diario y las medias y bajas quedan registradas para la revisión trimestral. El equipo vuelve a poder fusionar, que es la condición para poder arreglar nada.

Semana 2 — el golpe de mayor rendimiento. Actualizar la imagen base a la última versión de parche de node:20.11.0-bookworm-slim. Buena parte de las vulnerabilidades del sistema base desaparece con esa sola línea, sin tocar código. Reconstruir y volver a medir.

Semana 3 — las críticas y altas restantes, una a una. Para cada una: ¿tiene parche? Actualizar. ¿No lo tiene pero no es alcanzable? A .trivyignore con motivo y fecha. ¿No tiene parche y es alcanzable? Ticket con dueño y mitigación provisional.

Semana 4 — cerrar la puerta. Activar el bloqueo solo para críticas y altas con parche disponible que sean nuevas respecto a main, siguiendo la política del apartado 4, y programar el escaneo completo en el pipeline nocturno para detectar CVE publicadas sobre código que no ha cambiado. La lección de fondo: primero se reduce el ruido, después se bloquea; al revés, la herramienta se desactiva sola.

Solución 2. Por gravedad:

  1. pull_request_target con checkout del código del PR y ejecución de npm ci (crítico). Cualquiera puede abrir un PR desde un fork con un postinstall que exfiltre todo el entorno. Corrección: cambiar a pull_request; si de verdad hacen falta privilegios, separar en dos workflows y no ejecutar nunca el código del PR con secretos delante.
  2. Credenciales estáticas de AWS expuestas a ese código (crítico). No caducan y quedan en manos del atacante indefinidamente. Corrección: eliminarlas y usar OIDC; y en un job de validación de PR, no dar acceso a AWS en absoluto.
  3. permissions: write-all (alto). El token puede empujar a main, publicar releases y modificar el repositorio. Corrección: permissions: { contents: read } en la cabecera y añadir solo lo imprescindible por job.
  4. cualquiera/setup-tool@main (alto). Referenciar una rama de un tercero es ejecutar lo que ese repositorio contenga en cada ejecución. Corrección: fijar por SHA con comentario de versión.
  5. actions/checkout@v4 por etiqueta (medio). Aceptable en una acción oficial; en un entorno estricto, también por SHA.
  6. Falta timeout-minutes (bajo, pero real): un job comprometido puede minar criptomonedas durante seis horas antes de que nadie lo note.

Solución 3. Con lo construido, la respuesta sale en minutos y en tres pasos. (1) ¿Estamos afectados? Se consulta el SBOM de las imágenes desplegadas —adjunto a cada imagen en ECR y localizable por digest— y se busca la librería; como el SBOM se generó sobre el artefacto, incluye las transitivas y no solo las declaradas. En local, npm why <libreria> da la cadena que la introduce. (2) ¿Qué está desplegado? El endpoint /version de cada entorno da el commit exacto en producción, y la tabla despliegues de la 03-06 da el historial completo. (3) ¿Desde cuándo? Cruzando el primer commit cuyo SBOM contiene la versión vulnerable con la fecha de su despliegue a prod se obtiene la ventana de exposición, que es el dato que hará falta para cualquier comunicación a clientes.

Sin SBOM, los pasos 1 y 3 se vuelven trabajo manual y poco fiable: habría que reconstruir cada versión antigua —si es que sus dependencias siguen disponibles— o instalar a partir del lockfile de cada commit e inspeccionar el resultado, con horas de trabajo y ninguna garantía sobre lo que había dentro de la imagen base, que el lockfile no describe. Esa diferencia —minutos frente a un día, certeza frente a estimación— es el argumento entero a favor del SBOM, y solo se aprecia el día que hace falta; por eso se genera antes de necesitarlo.

Conclusión

El pipeline ha dejado de ser un punto ciego. Reservalia entiende ahora que su sistema de entrega es infraestructura crítica: tiene credenciales de producción, ejecuta código de terceros y publica los artefactos en los que todo el mundo confía. Sobre la primera mitad del problema —el software que pasa por el pipeline— tiene un job seguridad en ci.yml que corre en paralelo y en dos minutos ejecuta gitleaks sobre el historial completo, CodeQL como SAST, npm audit como SCA y Trivy sobre la imagen, con el escaneo profundo y el DAST desplazados al pipeline nocturno. Y, más importante que las herramientas, tiene una política de severidades que distingue lo nuevo de lo heredado, da dueño y plazo a cada categoría, trata los secretos como la única urgencia sin matices y documenta cada exclusión con motivo y caducidad: el diseño que evita que el equipo aprenda a ignorar el rojo.

Sobre la segunda mitad —el pipeline mismo—, las decisiones que parecían de comodidad son ahora decisiones de seguridad conscientes. OIDC no es un secreto mejor guardado, es la ausencia de secreto; los secretos que quedan viven en el entorno que los necesita y no en el repositorio, con rotación trimestral ensayada; los permisos son mínimos y explícitos, tanto en el token de GitHub como en los roles de AWS; las acciones de terceros se fijan por SHA para que una etiqueta movida no cambie lo que se ejecuta; pull_request_target no se usa; y las reglas de protección de rama, la firma de commits y los registros de auditoría de GitHub y CloudTrail cierran el círculo. Hay además un procedimiento escrito para lo que de verdad ocurre: rotar primero, investigar después, limpiar el historial al final. La cadena de suministro queda cerrada con tres respuestas —qué contiene un artefacto (SBOM con Syft, adjuntado a la imagen), quién lo produjo (firma con cosign, verificada antes de desplegar) y cómo se produjo (atestaciones de procedencia)—, situadas en el nivel 2 de SLSA de forma deliberada. Y con la advertencia intacta: esto son controles automatizados, no un dictamen de seguridad; para eso hace falta un profesional que audite el sistema real y alguien que conozca las obligaciones legales de tu organización.

Con esto hay dos de los cuatro frentes resueltos, y el pipeline hace más cosas que nunca: cinco jobs, escaneos, firmas e inventarios. Lo cual reabre el primer frente con más fuerza, porque cada control añadido cuesta minutos y Diego ya lo dijo —"si el CI tarda más que ir a por un café, dejo de mirarlo"—. La siguiente lección, Escalabilidad y Rendimiento, mide dónde se va exactamente el tiempo del pipeline de Reservalia y lo recorta con las palancas ordenadas por relación coste/beneficio —caché, paralelización, ejecución selectiva y elección del runner—, sin perder de vista el riesgo que las acompaña a todas: que una optimización mal hecha produzca un verde falso, un pipeline rapidísimo que ya no comprueba lo que creemos que comprueba.

Curso de CI/CD: Integración y Despliegue Continuo

Módulo 1: Introducción a CI/CD

Módulo 2: Integración Continua (CI)

Módulo 3: Despliegue Continuo (CD)

Módulo 4: Prácticas Avanzadas de CI/CD

Módulo 5: Implementación de CI/CD en Proyectos Reales

Módulo 6: Herramientas y Tecnologías

Módulo 7: Ejercicios Prácticos

Módulo 8: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados