Módulo 4: Policy As Code With Conftest
1. Introducción: de la revisión humana del `plan` a la política automática
Descripción
RISK-MAP.md cerró el Módulo 1 con siete filas. Dos de ellas, TM-04 y TM-06, tienen la misma columna Módulo: M4. Este es ese módulo, y es —según el propio diseño de esta guía— el de más peso ejecutado: cada comando que vas a ver en las próximas ocho lecciones corrió de verdad para escribirlas, sobre conftest real y el terraform plan real de andes-cargo-infra/. No hay una sola lección "representativa" en todo este módulo.
Este módulo también cierra una promesa que dos guías hermanas hicieron y dejaron pendiente, a propósito. terraform-and-iac-guide, Módulo 8, lección 4, después de mostrarte cómo leer un plan como si fuera la revisión de un pull request, escribió esto, textual: "Esta guía nombra conftest y policy-as-code aquí, deliberadamente, sin construir ningún sistema de políticas... es exactamente el terreno de cloud-security-and-guardrails-guide, la guía hermana que retoma este punto exacto y lo construye de punta a punta". cicd-and-gitops-on-aws-guide hace la misma promesa desde otro ángulo: el propio diseño de esta guía cita su delegación textual — "Seguridad del pipeline a fondo (...) Sentinel/OPA como sistema de políticas) → cloud-security-and-guardrails-guide". Dos guías, dos menciones de pasada, ninguna política construida. Esta es la guía, y este es el módulo, donde esa política se escribe, se corre, y falla o pasa de verdad.
Conexión con el módulo
Este módulo tiene ocho lecciones y se leen en tres bloques. Las lecciones 2 y 3 son la base conceptual y la instalación real de la herramienta: qué es Rego (2), y conftest instalado y verificado en tu propia máquina (3). Las lecciones 4 y 5 construyen el puente completo, de lo más simple a lo real: una política trivial sobre un YAML de prueba (4), y el terraform plan de andes-cargo-infra/ convertido a JSON como la entrada real que conftest va a evaluar de aquí en adelante (5). Las lecciones 6 y 7 son las políticas que le dan cuerpo a este módulo: la que nunca deja destruir Shipments (6), y las dos que cierran mínimo privilegio y buckets públicos (7). La lección 8 reúne las tres en una sola biblioteca, policy/, corrida contra un cambio que pasa y uno que se detiene.
El mapa de este módulo: las 8 lecciones
MÓDULO 4 — POLICY-AS-CODE CON CONFTEST
M4.1 Introducción (esta) mapa del módulo; las dos promesas que cierra
M4.2 Qué es OPA y Rego deny[msg]/deny contains msg; Sentinel por contraste
M4.3 Manos a la obra: instalando conftest EJECUTADO — conftest --version real
M4.4 Manos a la obra: tu primera política Rego EJECUTADO — PASS y FAIL sobre un YAML de prueba
M4.5 De HCL a JSON: el plan como entrada EJECUTADO — terraform show -json, resource_changes
M4.6 La política "nunca destruir Shipments" EJECUTADO — reemplaza el grep de cicd M6.7
M4.7 Mínimo privilegio y buckets públicos EJECUTADO — dos políticas más, corridas sobre el plan real
M4.8 Proyecto: la biblioteca de políticas EJECUTADO — policy/ completo, pasa y falla a propósito
| # | Lección | Qué construye |
|---|---|---|
| 1 | Introducción (esta) | El mapa del módulo; las dos promesas de guías hermanas que este módulo cumple |
| 2 | Qué es Open Policy Agent y Rego | El lenguaje de políticas, con un ejemplo mínimo; Sentinel nombrado por contraste |
| 3 | Manos a la obra: instalando conftest | Ejecutado: binario real, conftest --version confirmado |
| 4 | Manos a la obra: tu primera política Rego | Ejecutado: PASS y FAIL reales sobre un YAML de prueba, antes de tocar Terraform |
| 5 | De HCL a JSON: el terraform plan como entrada | Ejecutado: terraform plan -out=tfplan && terraform show -json, estructura explorada |
| 6 | Manos a la obra: la política "nunca destruir Shipments" | Ejecutado: no-destroy-shipments.rego, falla el intento real, pasa el cambio inocuo |
| 7 | Manos a la obra: mínimo privilegio y buckets públicos como política | Ejecutado: least-privilege-iam.rego + no-public-buckets.rego sobre el plan real |
| 8 | Proyecto: la biblioteca de políticas de Andes Cargo | Ejecutado: policy/ completo, corrido contra un plan que pasa y uno que falla a propósito |
Qué heredas, sin que se vuelva a explicar
Este módulo asume, sin repetirlo:
- De
terraform-and-iac-guide: el proyectoandes-cargo-infra/completo — los doce recursos de negocio, los módulosmodules/s3-bucket/ymodules/iam-role/, y el hábito de leer unterraform plancon atención antes de aprobarlo (Módulo 8, lección 4 de esa guía, el ejercicio exacto que este módulo automatiza). - De
cicd-and-gitops-on-aws-guide: el pipeline.github/workflows/corriendo bajoact, y el guardrail artesanal de su Módulo 6 — ungrepsobre elplanen JSON que detecta si algo destruyeShipments. Esta lección no lee ese módulo directamente (se escribe en paralelo a esta guía); lo que hereda es el contrato que el diseño de esta guía ya fija: esegrepa mano es exactamente lo que la lección 6 de este módulo reemplaza por una política Rego real. - De este mismo curso, Módulo 2:
andes-cargo-infra/conmodules/oidc-provider/aplicado y los dos roles reales —LambdaManifestProcessorRole,AppServerRole— ya recortados a mínimo privilegio (TM-07, resuelto en M2.7). Este módulo no vuelve a tocar esos dos roles; los usa como el plan real sobre el que corren las políticas de la lección 7. - De este mismo curso, Módulo 1:
THREAT-MODEL.mdyRISK-MAP.md. Este módulo cierra dos filas —TM-04(bucket sin bloqueo de acceso público) yTM-06(ningún control detiene unplanque destruyeShipments) — ambas asignadas aM4desde queRISK-MAP.mdse escribió.
Las dos filas de RISK-MAP.md que este módulo cierra
| Order | ID | STRIDE | Riesgo | Control | Módulo |
|---|---|---|---|---|---|
| 4 | TM-04 | Information disclosure | Bucket S3 sin bloqueo de acceso público | no-public-buckets.rego | M4 |
| 5 | TM-06 | Denial of service | Ningún control detiene un plan destructivo sobre Shipments | no-destroy-shipments.rego | M4 |
Fíjate en el orden dentro de la propia tabla: TM-06 —la política que la lección 6 de este módulo construye— aparece antes que TM-04 —que la lección 7 construye— en la secuencia de lecciones, aunque RISK-MAP.md las liste en el orden inverso. Esto no es una inconsistencia: RISK-MAP.md ordena por severidad de consecuencia (perder la tabla Shipments completa es más grave que un bucket sin bloquear), mientras que este módulo enseña primero la política más simple de razonar —una sola tabla, una sola condición— antes de las dos políticas de la lección 7, que necesitan iterar sobre Statement anidados dentro de una política IAM. Vas a confirmar tú mismo, en la lección 6, por qué el orden pedagógico y el orden de severidad no siempre coinciden.
Lo que este módulo agrega a andes-cargo-infra/
Un directorio nuevo, policy/, con tres archivos .rego — y una sola adición real de infraestructura: el recurso aws_s3_bucket_public_access_block que cierra TM-04, escrito en la lección 7. Nada más de negocio cambia.
andes-cargo-infra/
├── THREAT-MODEL.md (M1, sin cambios de contenido)
├── RISK-MAP.md (M1 → este módulo actualiza TM-04 y TM-06 a Resolved)
├── s3.tf (heredado — M4.7 agrega aws_s3_bucket_public_access_block)
├── modules/ (heredado, sin cambios)
└── policy/ ← NUEVO de este módulo
├── no-destroy-shipments.rego M4.6
├── least-privilege-iam.rego M4.7
└── no-public-buckets.rego M4.7
El compromiso de honestidad de este módulo específico
A diferencia del Módulo 2 —donde tres de ocho lecciones tenían un tramo representativo por el límite exacto de LocalStack Hobby—, este módulo no necesita ese matiz. conftest es un binario que corre en tu máquina, contra un archivo JSON en tu disco; no habla con ninguna nube, no necesita ningún emulador, no tiene un plan de pago que desbloquee más rigor. terraform plan —a diferencia de terraform apply— tampoco necesita infraestructura real detrás: calcula la diferencia entre tu configuración y el estado que ya tiene, sin tocar ningún recurso remoto. Por eso este módulo puede prometer algo que casi ningún otro módulo de esta guía puede: terraform apply y awslocal no aparecen en ninguna de las ocho lecciones. Todo lo que ves corrió, literal, sobre el motor real de ambas herramientas — conftest en su versión 0.69.0, Terraform CLI en su versión 1.15.8.
Vas a encontrar, sí, un hallazgo real que ninguna guía anterior de este ecosistema documentó todavía: la sintaxis de Rego que quizás recuerdes de tutoriales antiguos (deny[msg] { ... }, sin la palabra if) ya no compila contra las versiones actuales del motor. La lección 4 te muestra el error real, literal, y la sintaxis que sí funciona — parte de la misma honestidad que gobierna toda esta guía: nada se corrige en silencio, todo lo que cambió desde que aprendiste algo se documenta en el momento en que aparece.
Errores comunes
Asumir que este módulo necesita LocalStack corriendo, como los módulos 2 y 3 (de expectativa). Qué pasa: alguien, acostumbrado al patrón tflocal apply + awslocal de los módulos anteriores, arranca un contenedor de LocalStack antes de empezar este módulo. Cómo detectarlo: si tu primer paso al abrir este módulo fue docker run localstack/localstack. Cómo corregirlo: no hace falta — cada comando de este módulo es terraform plan, terraform show -json, o conftest test, ninguno de los cuales necesita una nube (real ni emulada) del otro lado. Puedes cerrar el contenedor y ahorrarte los recursos.
Confundir "policy-as-code" con "esto reemplaza la revisión humana del plan" (de alcance). Qué pasa: alguien concluye que, con conftest corriendo, ya nadie necesita leer un plan con atención — la lección 4 de terraform-and-iac-guide Módulo 8 queda obsoleta. Cómo detectarlo: si tu conclusión de este módulo es "ya no hace falta que un humano mire nunca más un plan". Cómo corregirlo: conftest automatiza las reglas que ya sabes que quieres exigir siempre — nunca destruir Shipments, nunca una política con Action: "*" — pero no reemplaza el juicio humano frente a un cambio que ninguna regla anticipó. La disciplina de leer un plan sigue siendo la primera línea de defensa; conftest es la segunda, la que corre incluso cuando nadie estuvo mirando con suficiente atención esa tarde específica.
Esperar que las políticas de este módulo cubran secretos o escaneo de vulnerabilidades (de solapamiento con módulos vecinos). Qué pasa: alguien busca, en policy/, una regla que detecte una credencial en texto plano o una dependencia vulnerable. Cómo detectarlo: si tu pregunta al terminar este módulo es "¿y dónde revisa conftest los secretos?". Cómo corregirlo: ese trabajo pertenece a otras dos capas completamente distintas — el Módulo 3 (SSM Parameter Store, Secrets Manager, y el escaneo de secretos con Trivy) y el Módulo 5 (Trivy/Checkov sobre configuración de IaC). conftest, en esta guía, hace un solo trabajo, y lo hace a fondo: evaluar reglas que tú mismo escribiste sobre la estructura de un terraform plan — nada de lo que Trivy o Checkov detectan con reglas de la comunidad.
Ejercicios
Ejercicio 1 — Ubica las dos filas exactas de RISK-MAP.md que este módulo cierra, y explica el orden invertido. Sin mirar la tabla de esta lección, escribe de memoria los IDs TM- de las dos filas que este módulo resuelve, y explica en una frase por qué la lección 6 construye la política de TM-06 antes que la lección 7 construya la de TM-04, aunque RISK-MAP.md las liste en el orden TM-04 antes de TM-06.
Ver solución
Las dos filas son TM-04 (bucket S3 sin bloqueo de acceso público, resuelto por no-public-buckets.rego) y TM-06 (ningún control detiene un plan que destruye Shipments, resuelto por no-destroy-shipments.rego). RISK-MAP.md las ordena por severidad de consecuencia — perder la tabla completa (TM-06) es, en la sección Decision de ese documento, un caso más concreto y evidenciado que un bucket sin bloquear (TM-04) —, pero este módulo las enseña en el orden inverso por una razón puramente pedagógica: no-destroy-shipments.rego (lección 6) razona sobre una sola condición en un solo tipo de recurso, mientras que las políticas de la lección 7 necesitan iterar sobre Statement anidados dentro de un documento de política IAM — una complejidad de Rego mayor que conviene enseñar después de la primera política, no antes.
Ejercicio 2 — Explica, a un colega que solo hizo terraform-and-iac-guide, qué cambia aquí respecto a lo que esa guía nombró. Tu colega te dice: "Ya vi mencionado conftest en el Módulo 8 de esa guía, con una regla en prosa sobre aws_s3_bucket_public_access_block. ¿Qué hay de nuevo en este módulo?". Respóndele en dos o tres frases.
Ver solución
Una respuesta completa suena, más o menos, así: "Esa guía te mostró, en prosa, qué condición debería fallar el pipeline — nunca escribió una sola línea de Rego, ni corrió conftest una sola vez, porque ese trabajo pertenecía deliberadamente a esta guía hermana. Este módulo construye exactamente esa política que quedó en prosa: no-public-buckets.rego, escrita en Rego real, corrida con conftest test contra el plan real de Andes Cargo, con un resultado literal —PASS o FAIL— que puedes ver en tu propia terminal. La diferencia es la misma que hay entre describir una regla y tener el árbitro que la aplica."
Ejercicio 3 — Predice qué pasaría si corrieras conftest test contra el plan de Andes Cargo tal como quedó al cierre del Módulo 2, antes de que este módulo agregue ningún archivo .rego. Sin haber leído todavía las lecciones 6 y 7, ¿qué esperarías que pasara si intentaras correr conftest test tfplan.json -p policy/ en ese punto exacto del proyecto?
Ver solución
Fallaría, pero no porque el plan tenga ningún problema — fallaría porque el directorio policy/ todavía no existe, o existe vacío: conftest necesita al menos un archivo .rego para evaluar, y sin ninguna regla escrita, no tiene nada contra qué comparar el plan. Es un recordatorio útil de que "policy-as-code" no es una propiedad automática de tener un plan en JSON — la protección solo existe a partir del momento exacto en que alguien escribe la regla, ni un minuto antes. Ese es, literalmente, el trabajo de las lecciones 6 y 7 de este módulo.
Resumen y siguiente paso
En esta lección viste el mapa completo de las ocho lecciones de este módulo, confirmaste qué heredas sin que se vuelva a explicar —el proyecto andes-cargo-infra/ completo, con modules/oidc-provider/ y los dos roles ya recortados del Módulo 2, y el guardrail artesanal de cicd-and-gitops-on-aws-guide que la lección 6 va a reemplazar—, y confirmaste las dos filas exactas de RISK-MAP.md que este módulo cierra. Viste también, de entrada, por qué este es el módulo con más ejecución real de toda la guía: ni terraform apply ni awslocal aparecen en ninguna de las ocho lecciones que siguen.
Antes de avanzar deberías poder: nombrar las ocho lecciones en orden y qué construye cada una; explicar por qué este módulo, a diferencia del Módulo 2, no tiene ningún tramo representativo; y ubicar las dos filas exactas de RISK-MAP.md que va a cerrar.
La lección 2 abre con la pregunta que este módulo entero contesta con código: ¿qué es, exactamente, Open Policy Agent, y qué es Rego, el lenguaje en el que vas a escribir tu primera política antes de que termine la lección 4?
Recursos
terraform-and-iac-guide, Módulo 8, lección 4 — la lectura de unplancomo revisión de PR, y la promesa exacta que este módulo cumple.cicd-and-gitops-on-aws-guide(guía hermana, en escritura paralela) — el guardrail artesanal (grepsobre elplanen JSON) que la lección 6 de este módulo reemplaza por Rego.- Este curso, Módulo 1, lección 8 (
RISK-MAP.md) — las dos filas (TM-04,TM-06) que este módulo resuelve. - Este curso, Módulo 2, lección 7 — los dos roles reales de Andes Cargo (
LambdaManifestProcessorRole,AppServerRole) que la lección 7 de este módulo usa, ya recortados a mínimo privilegio. - Conftest — Documentación oficial — la herramienta que este módulo construye de punta a punta, versión
0.69.0.