Extensión del laboratorio devsecops-pipeline para la ruta
Ingeniero DevSecOps. El recorrido base audita un pipeline
roto; este trayecto te pide construir uno que no lo esté y demostrar que funciona — incluida la
parte que casi nadie ensaya: revertirlo cuando tu propio control rompe los despliegues de toda la
empresa.
⚠️ Trabaja siempre sobre un repositorio tuyo (uno nuevo vale) o sobre una copia local de
repo-vulnerable/. Nunca uses credenciales reales: todo el material del laboratorio usa credenciales falsas con formato válido, a propósito (Clase 025).
1. Controles integrados en CI/CD → que corran solos, en el sitio correcto
2. Bloqueo proporcional → qué para el merge, qué para el deploy, qué solo avisa
3. Secretos y permisos del pipeline → identidad efímera, mínimo privilegio, nada de larga vida
4. Cobertura de análisis → código, dependencias, IaC, imagen y app en ejecución
5. SBOM → el inventario que salva el fin de semana
6. Firma y verificación → que lo desplegado sea lo que construiste
7. Policy as code → reglas versionadas, revisadas y probadas
8. Excepciones auditables y temporales → la válvula que evita que te desactiven el gate
9. Reversión demostrada → el control defectuoso también es un incidente
| Paso | Clases del programa |
|---|---|
| Pipelines y CI/CD | 242, 236 |
| Análisis automatizado | 238, 239, 240 |
| Secretos e identidad | 241, 233, 063 |
| Contenedores e IaC | 243, 227, 230, 229 |
| Cadena de suministro | 246 |
| Policy as code | 244 |
| Cultura y adopción | 248, 330 |
El toolbox del laboratorio trae las herramientas de auditoría. Las de construcción de este trayecto (SBOM, firma, policy as code) se instalan aparte y son opcionales por tramos:
| Capacidad | Herramienta de referencia | ¿Viene en el toolbox? |
|---|---|---|
| SAST | Bandit, Semgrep | Sí |
| SCA y escaneo de imagen | Trivy | Sí |
| Secretos | gitleaks | Sí |
| Dockerfile | hadolint | Sí |
| Workflows CI/CD | actionlint (zizmor si lo instalas) | Parcial |
| DAST | ZAP (imagen oficial) | No — se lanza por Docker |
| SBOM | Syft o Trivy (--format cyclonedx) |
Trivy sí, Syft no |
| Firma y verificación | Cosign | No |
| Policy as code | OPA / Conftest | No |
Aplica aquí el mismo principio que el resto del laboratorio: una capacidad que no pudiste ejecutar no es una capacidad limpia. Si no instalas Cosign, tu entrega dice "firma: no implementada", no "firma: correcta".
Coloca cada análisis donde su coste y su señal se compensan. Este es el criterio, y hay que poder defenderlo:
| Control | Dónde | Por qué ahí |
|---|---|---|
| Escaneo de secretos | pre-commit + CI en cada push | Cuesta milisegundos y el daño es inmediato |
| SAST incremental | Cada pull request, solo sobre el diff | Rápido y con contexto de revisión |
| SAST completo | Nocturno sobre la rama principal | Caro; no debe frenar a nadie |
| SCA | Cada push y en cada cambio de lockfile | Es donde aparece la mayoría del riesgo real |
| IaC y Dockerfile | Cada pull request que los toque | Barato y de alto rendimiento |
| Escaneo de imagen | En el build, antes de publicar | Publicar una imagen vulnerable es propagarla |
| DAST | Contra el entorno de pruebas desplegado | Necesita la aplicación en marcha |
Un esqueleto de workflow con lo esencial (adáptalo a tu plataforma de CI):
name: seguridad
on:
pull_request:
push:
branches: ["main"]
# Mínimo privilegio explícito: por defecto, solo lectura.
permissions:
contents: read
jobs:
analisis:
runs-on: ubuntu-latest
steps:
# Fija las acciones por SHA, no por etiqueta: una etiqueta se puede mover.
- uses: actions/checkout@<sha-completo>
- name: Secretos
run: |
docker run --rm -v "$PWD":/src zricethezav/gitleaks:v8.18.4 \
detect --no-git --source /src -v
- name: Dependencias e imagen base
run: |
docker run --rm -v "$PWD":/src aquasec/trivy:0.53.0 fs --exit-code 1 \
--severity HIGH,CRITICAL /src
- name: SAST
run: |
docker run --rm -v "$PWD":/src semgrep/semgrep:1.86.0 \
semgrep --config auto --error /src
Tres decisiones que ya están tomadas en ese esqueleto y que debes saber justificar: versiones
fijadas (una herramienta que se autoactualiza pone tu CI en rojo sin que nadie toque el
repositorio), permissions explícito y mínimo, y acciones fijadas por SHA.
Un gate que bloquea todo se desactiva en un mes, y entonces la seguridad es cero. Define tres niveles y escribe el porqué de cada umbral:
| Nivel | Qué lo activa | Efecto |
|---|---|---|
| Bloquea el merge | Secreto detectado · vulnerabilidad crítica nueva en dependencia directa · política de IaC violada | El pull request no se puede integrar |
| Bloquea el despliegue a producción | Imagen sin firmar · CVE crítica en KEV en el artefacto · SBOM ausente | Se construye pero no se publica |
| Solo avisa | Hallazgos medios y bajos · deuda preexistente · hallazgos de la rama nocturna | Comentario en el PR y entrada en el backlog |
La regla que hace viable todo lo anterior: línea base. El gate falla ante hallazgos nuevos, no ante los preexistentes ya triados por el Analista DevSecOps. Sin línea base, el primer día bloqueas a toda la empresa y te ganas una excepción permanente.
El pipeline es el sistema con más privilegio y menos vigilancia de la mayoría de las organizaciones. Cuatro medidas, en orden de rentabilidad:
permissions: contents: read por defecto; sube solo lo que un
job concreto necesite y solo en ese job.yaml
permissions:
contents: read
id-token: write # habilita OIDC; nada más
Comprobación de la capa 6 del recorrido base sobre tu propio workflow:
docker compose exec auditor ./auditar.sh workflows
Código propio, dependencias, IaC, imagen y aplicación en ejecución. Cada una ciega a lo que ven las otras:
# Código propio y dependencias (dentro del toolbox)
docker compose exec auditor ./auditar.sh sast deps secrets dockerfile container
# IaC, si tu proyecto tiene Terraform o manifiestos de Kubernetes
docker run --rm -v "$PWD":/src aquasec/trivy config /src
# DAST contra la aplicación desplegada en tu entorno de pruebas
docker run --rm --network host ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py -t http://127.0.0.1:8080 -r zap.html
🔒 El objetivo del DAST debe ser tu aplicación de pruebas, en tu máquina. Apuntar un escáner dinámico a un sistema ajeno sin autorización escrita es un delito, no una práctica.
Entrega la matriz de cobertura: qué superficie cubre cada herramienta y —más importante— qué queda sin cubrir. La lógica de negocio, por ejemplo, no la ve ninguna de las cinco.
El SBOM no es un trámite de cumplimiento: es lo que te permite responder "¿nos afecta?" en minutos cuando aparece una vulnerabilidad grave un domingo por la noche.
# Con Trivy (ya está en el toolbox)
docker compose exec auditor \
trivy fs --format cyclonedx --output /audit/salida/sbom.cdx.json /audit/repo
# Con Syft, si lo instalas
syft dir:. -o cyclonedx-json=sbom.cdx.json
syft dir:. -o spdx-json=sbom.spdx.json
Tres requisitos para que sirva de algo:
jq o con lo que uses. Si no puedes, todavía no tienes un inventario.Firmar responde a una pregunta que ningún escáner responde: ¿lo que corre en producción es lo que construiste?
# Firmar (modo sin claves: la identidad viene del proveedor OIDC)
cosign sign-blob --yes artefacto.tar.gz --bundle artefacto.bundle
# Verificar en el punto de despliegue
cosign verify-blob artefacto.tar.gz \
--bundle artefacto.bundle \
--certificate-identity-regexp '^https://github\.com/TU-ORG/.+$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
La parte que convierte esto en un control y no en un adorno: el despliegue debe fallar si la verificación falla. Demuéstralo — modifica un byte del artefacto y enseña el rechazo.
Sobre SLSA: no intentes saltar al nivel máximo. Sube un escalón y demuéstralo: build reproducible desde el código fuente, procedencia generada automáticamente por el propio sistema de CI (no por una persona) y verificación en el consumo. Documenta en qué nivel estás y qué falta para el siguiente.
Una política escrita en un documento se ignora; escrita como código se revisa, se versiona y se prueba como cualquier otra cosa.
policy/kubernetes.rego:
package kubernetes.security
import rego.v1
deny contains msg if {
input.kind == "Deployment"
c := input.spec.template.spec.containers[_]
not c.securityContext.runAsNonRoot
msg := sprintf("El contenedor '%s' no declara runAsNonRoot", [c.name])
}
deny contains msg if {
input.kind == "Deployment"
c := input.spec.template.spec.containers[_]
endswith(c.image, ":latest")
msg := sprintf("El contenedor '%s' usa la etiqueta ':latest'", [c.name])
}
policy/kubernetes_test.rego:
package kubernetes.security_test
import rego.v1
import data.kubernetes.security
test_rechaza_root if {
count(security.deny) == 1 with input as {
"kind": "Deployment",
"spec": {"template": {"spec": {"containers": [
{"name": "api", "image": "api:1.4.2"}
]}}}
}
}
Ejecución:
opa test policy/ -v # las políticas también se prueban
conftest test k8s/deployment.yaml -p policy/
Regla de oficio: una política sin pruebas es una política que algún día bloqueará lo que no debía. Y las pruebas son, además, la documentación que entiende quien desarrolla.
Si tu gate no tiene válvula, la válvula la construirá otro: desactivándolo. Diséñala tú.
.security-exceptions.yml en el repositorio, revisado como código:
- id: EXC-2026-011
regla: trivy/CVE-2023-88888
ambito: servicios/facturacion
motivo: sin versión corregida publicada; sustitución estimada en 3 semanas
solicita: equipo-pagos
aprueba: jefatura-ingenieria
compensacion: endpoint restringido a red interna + validación estricta de entrada
vence: 2026-11-30
Cuatro propiedades que la hacen aceptable:
Implementa la comprobación de vencimiento y demuéstrala: pon una fecha pasada y enseña el fallo.
El paso que separa a quien construye plataformas de quien añade pasos a un YAML. Tu control va a fallar algún día; lo que se evalúa es cómo se comporta la plataforma ese día.
Ejercicio completo:
Métricas del ejercicio: tiempo hasta la detección, tiempo hasta la reversión y número de equipos afectados. Anótalas. Son exactamente las que te van a preguntar en una entrevista.
opa test pasa y la política rechaza un manifiesto real
del laboratorio.cloud-security ·
appsec-code · appsec-web