DEV Community

Cover image for Trivy en CI/CD: bloqueando imágenes Docker vulnerables antes del deploy
Coles C
Coles C

Posted on

Trivy en CI/CD: bloqueando imágenes Docker vulnerables antes del deploy

Construir una imagen Docker y desplegarla automáticamente es sencillo.

El problema aparece cuando esa imagen contiene vulnerabilidades conocidas y nuestro pipeline la envía a producción sin comprobar nada.

Un pipeline tradicional puede verse así:

Code
  ↓
Build
  ↓
Test
  ↓
Docker Build
  ↓
Push
  ↓
Deploy
Enter fullscreen mode Exit fullscreen mode

Desde una perspectiva DevSecOps, falta algo importante:

Code
  ↓
Build
  ↓
Test
  ↓
Docker Build
  ↓
Security Scan
  ↓
Push
  ↓
Deploy
Enter fullscreen mode Exit fullscreen mode

En este artículo vamos a usar Trivy para analizar una imagen Docker y hacer que GitHub Actions detenga el pipeline si encuentra vulnerabilidades que no cumplen nuestra política de seguridad.


¿Qué es Trivy?

Trivy es un scanner de seguridad open source desarrollado por Aqua Security.

Puede analizar diferentes tipos de objetivos, incluyendo:

  • Container images
  • Filesystems
  • Git repositories
  • Kubernetes
  • Infrastructure as Code

En el caso de las imágenes Docker, Trivy inspecciona el contenido de la imagen buscando vulnerabilidades conocidas.

Por ejemplo, localmente podríamos ejecutar:

trivy image nginx:latest
Enter fullscreen mode Exit fullscreen mode

Y obtener resultados similares a:

Library        Vulnerability     Severity
openssl        CVE-XXXX-XXXX     HIGH
libssl         CVE-XXXX-XXXX     CRITICAL
curl           CVE-XXXX-XXXX     MEDIUM
Enter fullscreen mode Exit fullscreen mode

Pero ejecutar Trivy manualmente no es suficiente.

Queremos que forme parte del pipeline.


Nuestro objetivo

Queremos construir el siguiente flujo:

Developer
    ↓
git push
    ↓
GitHub Actions
    ↓
Docker Build
    ↓
Trivy Scan
    ↓
┌─────────────────────────┐
│ Vulnerabilities found?  │
└────────────┬────────────┘
             │
       ┌─────┴─────┐
       │           │
      YES          NO
       │           │
       ↓           ↓
 Pipeline FAIL    Deploy
Enter fullscreen mode Exit fullscreen mode

De esta forma, una imagen vulnerable puede ser bloqueada antes de llegar al entorno de destino.


1. Una aplicación Docker simple

Imaginemos una aplicación con este Dockerfile:

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "app.py"]
Enter fullscreen mode Exit fullscreen mode

Podemos construirla utilizando:

docker build -t my-app:latest .
Enter fullscreen mode Exit fullscreen mode

Hasta aquí no sabemos realmente qué vulnerabilidades existen dentro de esa imagen.

Ahí entra Trivy.


2. Escanear la imagen localmente

Podemos ejecutar:

trivy image my-app:latest
Enter fullscreen mode Exit fullscreen mode

Trivy analizará la imagen y reportará las vulnerabilidades detectadas.

También podemos filtrar únicamente determinadas severidades:

trivy image \
  --severity HIGH,CRITICAL \
  my-app:latest
Enter fullscreen mode Exit fullscreen mode

Esto reduce el ruido y permite concentrarnos en vulnerabilidades que nuestra organización considera especialmente relevantes.

Pero todavía tenemos un problema.

Encontrar una vulnerabilidad no significa automáticamente que Trivy termine con error.

Para utilizarlo como un verdadero gate de CI/CD debemos indicarle que devuelva un código de salida diferente de cero cuando encuentre vulnerabilidades que coincidan con nuestra política.


3. Convertir Trivy en un Security Gate

Podemos ejecutar:

trivy image \
  --severity HIGH,CRITICAL \
  --exit-code 1 \
  my-app:latest
Enter fullscreen mode Exit fullscreen mode

Ahora nuestra política es:

No HIGH/CRITICAL vulnerabilities
        ↓
exit code 0
        ↓
Pipeline continues


HIGH/CRITICAL vulnerability
        ↓
exit code 1
        ↓
Pipeline fails
Enter fullscreen mode Exit fullscreen mode

Eso convierte el scanner en un verdadero security gate.

Ya no estamos simplemente generando un reporte.

Estamos tomando una decisión automática dentro del pipeline.


4. Integrarlo con GitHub Actions

Ahora vamos a automatizarlo.

Creamos el archivo:

.github/workflows/security.yml
Enter fullscreen mode Exit fullscreen mode

Con el siguiente workflow:

name: Build and Security Scan

on:
  push:
    branches:
      - main

  pull_request:

jobs:
  security:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Build Docker image
        run: |
          docker build \
            -t my-app:${{ github.sha }} \
            .

      - name: Scan Docker image with Trivy
        uses: aquasecurity/trivy-action@v0.36.0
        with:
          image-ref: 'my-app:${{ github.sha }}'
          format: 'table'
          severity: 'HIGH,CRITICAL'
          exit-code: '1'
Enter fullscreen mode Exit fullscreen mode

Aquí hay cuatro parámetros importantes:

image-ref: 'my-app:${{ github.sha }}'
Enter fullscreen mode Exit fullscreen mode

Indica qué imagen queremos analizar.

format: 'table'
Enter fullscreen mode Exit fullscreen mode

Muestra los resultados en formato tabla dentro de los logs de GitHub Actions.

severity: 'HIGH,CRITICAL'
Enter fullscreen mode Exit fullscreen mode

Indica qué niveles de severidad queremos considerar.

exit-code: '1'
Enter fullscreen mode Exit fullscreen mode

Hace que Trivy termine con error cuando encuentra vulnerabilidades que coinciden con nuestro filtro.

Y ese último parámetro es el que convierte el scan en un security gate.


5. ¿Qué ocurre ahora?

Supongamos que hacemos:

git push
Enter fullscreen mode Exit fullscreen mode

GitHub Actions construirá una imagen:

my-app:<commit-sha>
Enter fullscreen mode Exit fullscreen mode

Después ejecutará Trivy.

Si no encuentra vulnerabilidades que coincidan con nuestra política:

Docker Build
   ✅

Trivy Scan
   ✅

Pipeline
   ✅
Enter fullscreen mode Exit fullscreen mode

Pero si encuentra una vulnerabilidad HIGH o CRITICAL:

Docker Build
   ✅

Trivy Scan
   ❌

Pipeline
   ❌
Enter fullscreen mode Exit fullscreen mode

Los siguientes pasos del job no se ejecutarán.

Nuestra imagen vulnerable deja de avanzar automáticamente por el pipeline.


6. ¿Debemos bloquear todas las vulnerabilidades?

Aquí es donde DevSecOps se vuelve más interesante.

Podríamos crear una política extremadamente estricta:

severity: 'LOW,MEDIUM,HIGH,CRITICAL'
exit-code: '1'
Enter fullscreen mode Exit fullscreen mode

En teoría parece una excelente idea.

En la práctica probablemente terminemos con pipelines constantemente bloqueados.

Una política inicial más razonable podría ser:

Severity Acción
LOW Informar
MEDIUM Informar
HIGH Bloquear
CRITICAL Bloquear

Es decir:

severity: 'HIGH,CRITICAL'
exit-code: '1'
Enter fullscreen mode Exit fullscreen mode

Pero esto tampoco debería considerarse una regla universal.

Cada organización debería definir su política teniendo en cuenta factores como:

  • Criticidad del servicio
  • Exposición a Internet
  • Disponibilidad de exploits
  • Disponibilidad de un parche
  • Controles compensatorios
  • Riesgo aceptado

DevSecOps no consiste simplemente en:

scanner found something
        ↓
fail everything
Enter fullscreen mode Exit fullscreen mode

Consiste en convertir requisitos de seguridad en controles automatizados y repetibles.


7. ¿Qué hacemos con vulnerabilidades sin solución?

También podemos decidir ignorar vulnerabilidades para las que todavía no existe un fix disponible.

Por ejemplo:

- name: Trivy Security Gate
  uses: aquasecurity/trivy-action@v0.36.0
  with:
    image-ref: 'my-app:${{ github.sha }}'
    format: 'table'
    severity: 'HIGH,CRITICAL'
    ignore-unfixed: true
    exit-code: '1'
Enter fullscreen mode Exit fullscreen mode

Esto permite implementar una política similar a:

HIGH/CRITICAL + fix available
           ↓
         BLOCK

HIGH/CRITICAL + no fix
           ↓
         REPORT
Enter fullscreen mode Exit fullscreen mode

Esto puede reducir falsos bloqueos operativos.

Sin embargo, ignorar vulnerabilidades sin parche también significa aceptar riesgo.

Por eso debe ser una decisión consciente de política de seguridad y no simplemente una configuración copiada de Internet.


8. Reporting y Enforcement son cosas diferentes

Una estrategia interesante es separar dos conceptos:

Visibility
Enter fullscreen mode Exit fullscreen mode

y:

Enforcement
Enter fullscreen mode Exit fullscreen mode

Podemos ejecutar dos escaneos.

Primero mostramos todas las vulnerabilidades:

- name: Security Report
  uses: aquasecurity/trivy-action@v0.36.0
  with:
    image-ref: 'my-app:${{ github.sha }}'
    format: 'table'
    severity: 'LOW,MEDIUM,HIGH,CRITICAL'
    exit-code: '0'
Enter fullscreen mode Exit fullscreen mode

Aquí queremos obtener información.

No queremos bloquear nada.

Después ejecutamos nuestro security gate:

- name: Security Gate
  uses: aquasecurity/trivy-action@v0.36.0
  with:
    image-ref: 'my-app:${{ github.sha }}'
    format: 'table'
    severity: 'HIGH,CRITICAL'
    exit-code: '1'
Enter fullscreen mode Exit fullscreen mode

Conceptualmente:

           Trivy
             │
      ┌──────┴──────┐
      │             │
   Reporting     Enforcement
      │             │
      ↓             ↓
All CVEs      HIGH / CRITICAL
                    │
                    ↓
               Block pipeline
Enter fullscreen mode Exit fullscreen mode

Esto proporciona visibilidad sobre el riesgo completo sin convertir cada vulnerabilidad menor en un bloqueo.


9. El pipeline completo

Finalmente nuestro workflow podría quedar así:

name: DevSecOps Pipeline

on:
  push:
    branches:
      - main

  pull_request:

jobs:
  build-and-scan:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Build image
        run: |
          docker build \
            -t my-app:${{ github.sha }} \
            .

      - name: Vulnerability report
        uses: aquasecurity/trivy-action@v0.36.0
        with:
          image-ref: 'my-app:${{ github.sha }}'
          format: 'table'
          severity: 'LOW,MEDIUM,HIGH,CRITICAL'
          exit-code: '0'

      - name: Security gate
        uses: aquasecurity/trivy-action@v0.36.0
        with:
          image-ref: 'my-app:${{ github.sha }}'
          format: 'table'
          severity: 'HIGH,CRITICAL'
          exit-code: '1'

      - name: Deploy
        run: |
          echo "Deploying application..."
Enter fullscreen mode Exit fullscreen mode

Ahora el deploy solamente ocurre cuando la imagen pasa nuestra política de seguridad.

Nuestro pipeline queda conceptualmente así:

Git Push
   ↓
Checkout
   ↓
Docker Build
   ↓
Trivy Report
   ↓
Trivy Security Gate
   ↓
     ┌───────────────┐
     │               │
   PASS             FAIL
     │               │
     ↓               ↓
   Deploy          STOP
Enter fullscreen mode Exit fullscreen mode

10. Security scanning no es DevSecOps

Añadir Trivy al pipeline es útil.

Pero instalar un scanner no convierte automáticamente nuestro proceso en DevSecOps.

La parte importante es definir:

What do we scan?
        ↓
When do we scan?
        ↓
What represents unacceptable risk?
        ↓
What automatically blocks deployment?
Enter fullscreen mode Exit fullscreen mode

La herramienta ejecuta el control.

La política define el control.

Eso es mucho más importante que simplemente añadir otro scanner a nuestro stack.


Conclusión

Trivy nos permite añadir una capa de seguridad relativamente sencilla dentro de nuestros pipelines CI/CD y detectar vulnerabilidades conocidas en nuestras imágenes Docker antes de desplegarlas.

Pero el objetivo no debería ser simplemente generar otro reporte de seguridad.

El verdadero cambio ocurre cuando hacemos que el pipeline pueda tomar decisiones:

Build
   ↓
Scan
   ↓
Evaluate Risk
   ↓
Security Gate
   ↓
Deploy
Enter fullscreen mode Exit fullscreen mode

Con este enfoque movemos los controles de seguridad hacia etapas anteriores del ciclo de desarrollo.

En lugar de descubrir una imagen vulnerable después de desplegarla:

hacemos que una imagen que incumple nuestra política nunca llegue al deploy.


Referencias

Top comments (0)