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/[email protected]
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 BloquearEs 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/[email protected]
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/[email protected]
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/[email protected]
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/[email protected]
with:
image-ref: 'my-app:${{ github.sha }}'
format: 'table'
severity: 'LOW,MEDIUM,HIGH,CRITICAL'
exit-code: '0'
- name: Security gate
uses: aquasecurity/[email protected]
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.