Módulo 8: Capstone The Andes Cargo Security Gate
4. Recorrido end-to-end: un cambio que cruza el gate completo
Descripción
La lección 3 confirmó que el gate existe y está bien encadenado. Esta lección le da un trabajo real: un cambio de negocio pequeño, honesto, sin ninguna intención de romper nada —una etiqueta de costo nueva en el bucket de manifiestos—, propuesto como un Pull Request real, evaluado por los cinco controles nuevos de esta guía (OIDC, secretos, conftest, Trivy, cosign), y llevado hasta el punto exacto donde apply.yml tomaría el relevo.
Conexión con el módulo
Esta es la mitad "positiva" de la tesis central del Módulo 8, la que el DISEÑO de esta guía fija como la prueba de que el gate no es puramente restrictivo: un cambio que debería pasar, pasa, sin fricción y sin que nadie tenga que pedirle una excepción a nada. La lección 5 es la otra mitad —un cambio que debería detenerse, se detiene—. Las dos, juntas, son la evidencia completa de que este security gate distingue correctamente entre lo uno y lo otro, no solo entre "cambia algo" y "no cambia nada".
Analogía: el pasajero con el pasaporte en regla
Un aeropuerto que solo demuestra que detiene a alguien sin documentos no ha demostrado que su sistema de controles funciona — ha demostrado que, al menos, no deja pasar lo obviamente malo. Un aeropuerto que funciona de verdad también deja pasar, sin demoras innecesarias, al pasajero con el pasaporte en regla, el equipaje sin nada prohibido, la tarjeta de embarque correcta. Esta lección es ese segundo pasajero: alguien que no está tratando de romper nada, y que el sistema reconoce como tal, en cada uno de los tres controles, sin pedirle una explicación adicional.
Paso 1 — El cambio: una etiqueta de costo en el bucket de manifiestos
Andes Cargo necesita, por una razón completamente ajena a la seguridad, empezar a rastrear el costo del bucket de manifiestos por centro de costo. Es el tipo de cambio que un equipo de FinOps pediría —fuera del alcance de esta guía (Módulo 7, frontera con finops-and-cost-guardrails-guide), pero perfectamente normal como tráfico real de un repositorio de infraestructura—:
tags = {
Environment = "dev"
ManagedBy = "terraform"
Project = "andes-cargo"
+ CostCenter = "logistics"
}
git diff -- s3.tf
Qué esperar (literal):
diff --git a/s3.tf b/s3.tf
index cce01db..56a1cfa 100644
--- a/s3.tf
+++ b/s3.tf
@@ -6,5 +6,6 @@ module "shipment_docs_bucket" {
Environment = "dev"
ManagedBy = "terraform"
Project = "andes-cargo"
+ CostCenter = "logistics"
}
}
Una línea. Ningún permiso IAM tocado, ninguna política de bucket modificada, ninguna dependencia nueva. Exactamente el tipo de cambio que un equipo esperaría que un gate de seguridad ignore —no porque el gate no lo revise, sino porque no hay nada en él que ningún control de este módulo esté diseñado para atrapar.
git add -A
git commit -m "Add CostCenter tag to the shipment docs bucket"
Paso 2 — Los cinco controles, uno por uno, antes del pipeline
Antes de correr el gate completo, vale la pena nombrar, explícitamente, los cinco controles nuevos que este cambio atraviesa —no todos corren un comando nuevo en esta lección, pero los cinco son parte de por qué este PR llega limpio hasta el final:
| Control | Módulo | Qué confirma sobre este cambio |
|---|---|---|
| Identidad OIDC | M2 | El workflow que ejecuta este PR se autentica con una identidad federada de vida corta, no con una clave de larga vida robada de un .secrets — el cambio nunca necesitó ninguna credencial estática para proponerse |
| Secretos gestionados | M3 | Ningún secreto en texto plano existe en este diff, ni en ningún archivo que este cambio toque — secrets.tf sigue intacto |
conftest (policy-check) | M4 | Las tres políticas evalúan el plan completo, no solo la línea que cambió — confirman que nada más en el proyecto se salió de línea al mismo tiempo |
Trivy (iac-scan) | M5 | Confirma que agregar una etiqueta no introdujo, por accidente, ningún hallazgo CRITICAL/HIGH nuevo |
cosign (verify-artifact) | M6 | Confirma que lambda/function.zip —sin relación alguna con este cambio de etiqueta— sigue siendo exactamente el artefacto que el equipo firmó |
Los primeros dos no corren ningún comando en esta lección específica —ya están construidos y verificados desde M2 y M3—, pero son la razón por la que este PR pudo proponerse en primer lugar, con una identidad de confianza y sin ningún secreto expuesto en el camino.
Paso 3 — El gate completo, de punta a punta
terraform plan -out=tfplan -input=false
Qué esperar (literal, ejecutado para escribir esta lección):
Plan: 18 to add, 0 to change, 0 to destroy.
Saved the plan to: tfplan
Nota sobre este número. Como en el Módulo 5, lección 7, y el Módulo 6, lección 8, esta lección corre sobre el laboratorio aislado de la lección 3 — un andes-cargo-infra/ recién clonado, sin ningún estado previo aplicado contra LocalStack. 18 to add es, por lo tanto, una creación completa del proyecto, no un diff incremental de una sola etiqueta contra infraestructura ya desplegada — el plan que un ci.yml real evaluaría en cualquiera de los dos casos, con la única diferencia real de este PR siendo la línea que el Paso 1 ya mostró con git diff.
act pull_request -e .github/act-events/pr-event.json
Qué esperar (literal — ejecutado para escribir esta lección, recortado a los resultados de cada job):
[ci/policy-check] ⭐ Run Main Evaluate the policy library against the plan
[ci/policy-check] | 4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions
[ci/policy-check] 🏁 Job succeeded
[ci/iac-scan ] ⭐ Run Main IaC security scan (Trivy)
[ci/iac-scan ] | Report Summary
[ci/iac-scan ] | ┌───────────────────────────┬───────────┬───────────────────┐
[ci/iac-scan ] | │ Target │ Type │ Misconfigurations │
[ci/iac-scan ] | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan ] | │ . │ terraform │ 0 │
[ci/iac-scan ] | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan ] | │ dynamodb.tf │ terraform │ 0 │
[ci/iac-scan ] | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan ] | │ lambda.tf │ terraform │ 0 │
[ci/iac-scan ] | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan ] | │ modules/s3-bucket/main.tf │ terraform │ 0 │
[ci/iac-scan ] | ├───────────────────────────┼───────────┼───────────────────┤
[ci/iac-scan ] | │ secrets.tf │ terraform │ 0 │
[ci/iac-scan ] | └───────────────────────────┴───────────┴───────────────────┘
[ci/iac-scan ] 🏁 Job succeeded
[ci/verify-artifact] ⭐ Run Main Verify function.zip against manifest.sig
[ci/verify-artifact] | WARNING: Skipping tlog verification is an insecure practice that lacks transparency and auditability verification for the blob.
[ci/verify-artifact] | Verified OK
[ci/verify-artifact] 🏁 Job succeeded
Los tres, otra vez, en verde. El resultado es idéntico al de la lección 3 —y eso es exactamente lo que se esperaría—: agregar una etiqueta a un bucket no cambia ninguna política IAM (policy-check no tiene nada nuevo que rechazar), no introduce ninguna configuración insegura (iac-scan no tiene nada nuevo que reportar), y no toca lambda/function.zip en absoluto (verify-artifact verifica el mismo artefacto de siempre, contra la misma firma de siempre).
Paso 4 — Hasta dónde llega este cambio: el punto exacto donde apply.yml tomaría el relevo
echo $?
0
Código de salida 0 del workflow completo — la señal que, en un repositorio real, habilitaría el botón de Merge en la interfaz de GitHub. (Representativo a partir de aquí.) Sin un token de LocalStack en este entorno de escritura, apply.yml no puede correr de verdad —la misma limitación honesta ya documentada desde el Módulo 1, lección 4—, pero el flujo que seguiría, si este PR se mergeara contra un repositorio con acceso real a LocalStack o a una cuenta AWS, es exactamente el que la lección 2 dibujó: push a main dispara apply.yml, que corre fetch-reviewed-plan y verify-artifact en paralelo (Stage 0, heredado del Módulo 6, lección 8), y solo si ambos terminan en éxito, terraform-apply (Stage 1) aplicaría la etiqueta nueva contra la infraestructura real.
git push origin feature/bucket-tag
# → PR abierto, ci.yml corre (Paso 3, arriba) → los tres jobs pasan
# → alguien aprueba y mergea el PR
# → push a main dispara apply.yml
# → fetch-reviewed-plan + verify-artifact (Stage 0) → ambos pasan
# → terraform-apply (Stage 1, representativo): CostCenter=logistics aplicado al bucket real
Errores comunes
Interpretar 18 to add como si este cambio hubiera creado dieciocho recursos nuevos. Qué pasa: alguien, viendo el resumen del plan, concluye que agregar una etiqueta generó diecisiete recursos adicionales de la nada. Cómo detectarlo: si tu explicación de este número no menciona que el laboratorio de esta lección parte de cero, sin ningún estado previo. Cómo corregirlo: la nota del Paso 3 ya lo explica — este número refleja una creación completa del proyecto en un laboratorio aislado, no un diff incremental. En un andes-cargo-infra/ con estado real ya aplicado desde los módulos anteriores, este mismo cambio de etiqueta mostraría Plan: 0 to add, 1 to change, 0 to destroy — una sola modificación, la que el git diff del Paso 1 ya muestra con precisión.
Asumir que un plan limpio en policy-check significa que Trivy o cosign tampoco tienen nada que decir. Qué pasa: alguien, después de ver policy-check pasar, asume que el resto del pipeline es una formalidad, sin valor real. Cómo detectarlo: si tu entendimiento de este PR es "conftest ya dijo que está bien, lo demás es trámite". Cómo corregirlo: los tres controles evalúan cosas distintas —política sobre el plan, configuración sobre el HCL crudo, integridad sobre un artefacto binario que no tiene ninguna relación con Terraform—. Que los tres coincidan en "PASS" para este cambio específico es una confirmación, no una redundancia: un cambio que rompiera solo el escaneo de Trivy (por ejemplo, deshabilitar el versionado del bucket) pasaría policy-check sin ningún problema, y sería iac-scan el único que lo detectaría.
Olvidar restaurar el estado del repositorio después de esta lección, antes de pasar a la lección 5. Qué pasa: alguien deja el commit de la etiqueta como el último del laboratorio, y empieza la lección 5 sin una base limpia. Cómo detectarlo: si git log muestra el commit de CostCenter como el más reciente al empezar la siguiente lección. Cómo corregirlo: no hace falta revertir nada para este caso específico —el cambio de esta lección es válido y puede quedarse mergeado, a diferencia del cambio de prueba de la lección 5, que sí necesita revertirse—. Solo confirma, antes de avanzar, que sabes distinguir cuál de los dos cambios de este módulo es "el que se queda" y cuál es "el que se prueba y se revierte".
Ejercicios
Ejercicio 1 — Repite este mismo recorrido con un cambio distinto, elegido por ti, que también debería pasar el gate sin fricción. Elige un cambio de negocio pequeño y honesto —otra etiqueta, un description más claro en un recurso, un timeout de Lambda ajustado a un valor razonable— y corre los tres pasos de esta lección contra él. ¿Pasó limpio, como esperabas?
Ver solución
Cualquier cambio que no toque una política IAM (evitando least-privilege-iam.rego), no cree un bucket sin su aws_s3_bucket_public_access_block correspondiente (evitando no-public-buckets.rego), no intente destruir la tabla Shipments (evitando no-destroy-shipments.rego), no introduzca una configuración insegura nueva (evitando Trivy), y no toque lambda/function.zip (evitando cualquier problema con cosign) debería pasar los tres jobs sin ningún hallazgo. El valor de este ejercicio no es el resultado —previsible, si elegiste bien el cambio— sino la práctica de predecir, antes de correr el gate, cuál de los tres controles (si alguno) podría objetar algo, y confirmar esa predicción con la corrida real.
Ejercicio 2 — Explica por qué OIDC (M2) y secretos (M3) están en la tabla del Paso 2, aunque ningún job de ci.yml los mencione explícitamente. Un compañero pregunta por qué la tabla del Paso 2 incluye dos controles que no corresponden a ningún job nombrado del pipeline (policy-check, iac-scan, verify-artifact).
Ver solución
Porque son precondiciones del pipeline entero, no controles que corren dentro de un job específico. OIDC (M2) es la razón por la que el propio ci.yml puede autenticarse contra una cuenta AWS sin usar una credencial de larga vida —sin él, este PR no tendría una forma segura de siquiera existir dentro de un pipeline real—. Secretos gestionados (M3) es la razón por la que ningún archivo de este repositorio, incluido este PR, necesita nunca contener una credencial en texto plano. Ninguno de los dos aparece como un job visible en ci.yml porque no son controles que evalúan un cambio propuesto — son la infraestructura de confianza sobre la que los tres controles visibles (policy-check, iac-scan, verify-artifact) operan. La lección 1 de este módulo ya lo señaló: los cinco controles nuevos no son cinco jobs — son cinco piezas, y solo tres de ellas se manifiestan como jobs de ci.yml.
Ejercicio 3 — Predice qué pasaría si este mismo cambio de etiqueta se propusiera en el mismo Pull Request que el intento de la lección 5 (ensanchar AppServerRole). Si alguien combinara ambos cambios —la etiqueta inocua y el ensanchamiento de permisos— en un solo commit, ¿el gate dejaría pasar la etiqueta mientras bloquea el permiso, o bloquearía el PR completo?
Ver solución
Bloquearía el PR completo — conftest test evalúa el plan entero de una sola corrida (exactamente como el Módulo 4, lección 8, ya demostró con dos violaciones simultáneas: 4 tests, 2 passed, 2 failures), no cambio por cambio dentro del mismo plan. Si la política least-privilege-iam.rego encuentra cualquier statement con Action: "*" en todo el plan, el resultado de conftest test es FAIL, y policy-check falla como job completo —no hay forma de que el pipeline distinga "esta parte del cambio está bien, esta otra no" dentro de un mismo Pull Request—. La consecuencia práctica es real: mezclar un cambio inocuo con uno riesgoso en el mismo PR bloquea a ambos por igual, otra razón concreta (además de las de buenas prácticas de revisión de código) para mantener los Pull Requests pequeños y enfocados en un solo cambio a la vez.
Resumen y siguiente paso
Esta lección tomó un cambio de negocio real, pequeño y sin ninguna intención de romper nada —una etiqueta de costo en el bucket de manifiestos— y lo llevó a través de los tres jobs del security gate, con el mismo ci.yml de la lección 3, sin ningún ajuste: policy-check, iac-scan y verify-artifact, los tres en verde. Nombraste, explícitamente, los cinco controles que este PR atraviesa —OIDC y secretos como precondiciones, conftest/Trivy/cosign como los tres jobs visibles—, y viste hasta dónde llega este cambio dentro del laboratorio $0 antes de que apply.yml (representativo, sin token) tomara el relevo.
La lección 5 es la contraparte exacta de esta: el mismo ci.yml, sin ningún cambio, contra un intento real de ensanchar AppServerRole — la prueba de que el gate distingue con precisión entre lo que debería pasar y lo que no.
Recursos
- Este curso, Módulo 4, lección 8 — la evidencia de que
conftest testevalúa elplancompleto de una sola corrida, la base del Ejercicio 3 de esta lección. - Este curso, Módulo 6, lección 8 — el job
verify-artifactdeapply.yml, el punto exacto donde este recorrido queda representativo. cicd-and-gitops-on-aws-guide, Módulo 5, lección 4 — el flujofetch-reviewed-plan→terraform-applyoriginal que este recorrido describe en su último paso.