Nota

DevSecOps en cinco etapas: del secret-scan a la admission policy

Cinco etapas en CI con exit-code 1 más un gate de admission en el cluster — el único patrón bajo el cual DevSecOps bloquea producción de verdad en vez de funcionar como un ritual de palomitas verdes.

La mayoría de los «pipelines DevSecOps» que reviso en equipos se rompen en el mismo punto: las comprobaciones están, pero corren en modo warn-and-continue. El escáner encuentra un CRITICAL — el pipeline queda en verde. Medio año después ya no es un pipeline; es un ritual.

Para que un pipeline de seguridad bloquee realmente producción hacen falta dos cosas: cinco etapas, cada una con exit-code 1 dictado por política, y un segundo gate dentro del cluster — en admission. Un gate de CI sin verificación en admission es una falsa sensación de seguridad: CI atrapa lo que construiste; el cluster acepta lo que desplegaste. Esos dos conjuntos se solapan, pero no coinciden.

Cinco etapas, y por qué exactamente cinco

Menos de cinco deja un hueco; más de cinco deja de ser un pipeline y se convierte en un programa aparte.

1. Secret scan       gitleaks / trufflehog / detect-secrets   fail ante cualquier coincidencia
2. SAST              Semgrep / SonarQube / CodeQL             fail ante nuevos HIGH/CRITICAL
3. Dependency scan   Trivy fs / Snyk / OWASP DC               fail con CVSS ≥ 7 sin VEX
4. Container scan    Trivy image / Grype                      fail ante HIGH/CRITICAL CVE sin fix
5. Manifest / IaC    Trivy config / Checkov / Conftest        fail ante misconfig de seguridad

La lógica es shift-left: cuanto antes se cace el defecto, más barato sale. Secretos antes de que entren al historial de git. SAST antes de que el código compile. Dependency-scan antes de que un paquete vulnerable acabe dentro de la imagen. Image-scan antes del push al registry. Manifest-scan antes de que kubectl apply llegue al cluster.

Cada etapa es un bucket de responsabilidad propio. Diluirlas en «un solo gran escáner» es el error clásico: cuando todo es Trivy, nadie se encarga del SAST. Cuando todo es SonarQube, nadie se encarga de la base de CVE. Equipos distintos, fallos distintos.

Etapa 1: secretos, y no solo en git

La base a nivel de git es gitleaks como hook pre-commit más detect-secrets con baseline en CI. La baseline hace falta: sin ella, cada AKIA* legítimo en un test rompe el build.

Hay una segunda capa que casi todo el mundo olvida — secretos cocidos en las capas de la imagen. RUN echo $TOKEN > .npmrc && ... && rm .npmrc deja el token en la capa inmutable de abajo; rm solo borra la vista superior. docker history --no-trunc o simplemente docker save | strings | grep token lo recupera del registry. Se cura con BuildKit secrets:

RUN --mount=type=secret,id=npmrc cp /run/secrets/npmrc ~/.npmrc && npm ci

Y en el pipeline — trivy image --scanners secret encima del gitleaks a nivel git. Dos escáneres distintos, dos superficies de ataque distintas.

Etapas 3–4: dependency y container no son lo mismo

trivy fs escanea package-lock.json, go.sum, requirements.txt — lo que vive en el código. trivy image escanea la imagen construida — ahí está todo lo que arrastró la capa base (apt-get install) y lo que pip install añadió encima. Una CVE en la libc de la imagen base no aparece bajo trivy fs. Una CVE en una dependencia de desarrollo que nunca llega a la imagen final no aparece bajo trivy image.

El canon en CI:

trivy image  --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed $IMAGE
trivy fs     --severity HIGH,CRITICAL --exit-code 1 .
trivy config --severity HIGH,CRITICAL --exit-code 1 ./k8s/

--ignore-unfixed es pragmatismo: mientras upstream no haya publicado un fix, bloquear el build no aporta nada. Pero convierte .trivyignore en deuda técnica — cada CVE suprimida necesita un comentario de why y un ticket en JIRA. Sin esa disciplina, un año después es un vertedero.

Etapa 5 + admission: el único patrón que aguanta

Etapa 5 en CI — trivy config / Checkov / Conftest sobre manifiestos y terraform plan -json. Caza lo que está en el repositorio. No caza lo que se desplegó esquivando CI ni un tag que fue sobrescrito — myapp:v1.2.3 en el registry podría haberse reconstruido hoy sin ningún escaneo.

Por eso el segundo gate corre en admission:

  • Kyverno verifyImages + cosign — la imagen se admite solo si CI la firmó tras pasar los escaneos. Build sin firma → admission deny.
  • OPA Gatekeeper / registry whitelist — pulls solo desde registries de confianza (ECR, Harbor, GHCR privado). Una imagen desde un docker.io/random/x público se rechaza.
  • ValidatingAdmissionPolicy (CEL) — desde K8s 1.36 integrado en el API server, sin webhook. Apto para políticas simples: nada de :latest, nada de privileged, etiquetas obligatorias. Lo complejo (image verify, multi-resource generate) sigue en Kyverno.

¿Duplicación? Deliberada. El escaneo en CI caza CVE en build-time; admission verify caza el deploy de una imagen que esquivó CI. Dos capas, cada una cubriendo la clase de fallo de la otra.

IaC: un pre-apply gate como última barrera

Terraform es una categoría aparte: un solo terraform apply crea decenas de recursos de golpe. El pre-apply gate:

terraform plan -out plan.binary
terraform show -json plan.binary > plan.json
conftest test plan.json --policy ./policies/   # OPA sobre el plan, no el código

Reglas en Rego: deny cidr_blocks = ["0.0.0.0/0"] en 22/3389; deny Action: "*" en IAM; deny s3_bucket sin server_side_encryption_configuration. Más barato fallar el PR que aplicar y revertir.

Una red estructural por debajo — un Deny de IAM sin la etiqueta ManagedBy=Terraform a nivel de cuenta: un engineer en la consola no puede tocar físicamente un recurso gestionado por IaC. El drift pasa de posible a imposible.

Lo que la gente olvida conectar

  • exit-code 1 en todas partes. «Soft-fail» en CI quiere decir «sin CI». Un quality gate de Sonar en modo failure (no warning) es obligatorio.
  • .trivyignore revisado en PR. Suprimir sin justificación es la forma más barata de desarmar el pipeline entero.
  • trivy db --skip-db-update en CI es un antipatrón. La base de CVE se actualiza ~3 veces al día; la de ayer es otra base.
  • Audit log sobre admission deny. Sin él, el gate de admission trabaja en silencio y nadie en el equipo ve que ayer Kyverno bloqueó tres deploys.

El verdadero cambio es dejar de pensar el DevSecOps como «configurar el escáner en CI». Son dos capas de política aplicadas a los mismos riesgos: shift-left en CI más admission-time enforce en el cluster. Una sin la otra es, o un ritual de palomitas verdes, o un kubeconfig abierto para cualquier imagen que esquivó el pipeline.