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
Desde una perspectiva DevSecOps, falta algo importante:
Code
↓
Build
↓
Test
↓
Docker Build
↓
Security Scan
↓
Push
↓
Deploy
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
Y obtener resultados similares a:
Library Vulnerability Severity
openssl CVE-XXXX-XXXX HIGH
libssl CVE-XXXX-XXXX CRITICAL
curl CVE-XXXX-XXXX MEDIUM
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
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"]
Podemos construirla utilizando:
docker build -t my-app:latest .
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
Trivy analizará la imagen y reportará las vulnerabilidades detectadas.
También podemos filtrar únicamente determinadas severidades:
trivy image \
--severity HIGH,CRITICAL \
my-app:latest
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
Ahora nuestra política es:
No HIGH/CRITICAL vulnerabilities
↓
exit code 0
↓
Pipeline continues
HIGH/CRITICAL vulnerability
↓
exit code 1
↓
Pipeline fails
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
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'
Aquí hay cuatro parámetros importantes:
image-ref: 'my-app:${{ github.sha }}'
Indica qué imagen queremos analizar.
format: 'table'
Muestra los resultados en formato tabla dentro de los logs de GitHub Actions.
severity: 'HIGH,CRITICAL'
Indica qué niveles de severidad queremos considerar.
exit-code: '1'
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
GitHub Actions construirá una imagen:
my-app:<commit-sha>
Después ejecutará Trivy.
Si no encuentra vulnerabilidades que coincidan con nuestra política:
Docker Build
✅
Trivy Scan
✅
Pipeline
✅
Pero si encuentra una vulnerabilidad HIGH o CRITICAL:
Docker Build
✅
Trivy Scan
❌
Pipeline
❌
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'
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'
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
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'
Esto permite implementar una política similar a:
HIGH/CRITICAL + fix available
↓
BLOCK
HIGH/CRITICAL + no fix
↓
REPORT
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
y:
Enforcement
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'
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'
Conceptualmente:
Trivy
│
┌──────┴──────┐
│ │
Reporting Enforcement
│ │
↓ ↓
All CVEs HIGH / CRITICAL
│
↓
Block pipeline
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..."
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
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?
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
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.
Top comments (0)