Módulo 6: Runtime Security Admission Control And Image Scanning

8. Proyecto: los guardrails de runtime de Andes Cargo

Descripción

Este proyecto cierra el módulo con dos piezas que las lecciones anteriores dejaron deliberadamente separadas. Primero, reactiva Gatekeeper (que la lección 6 puso en dryrun para aislar la prueba de Kyverno) y deja los dos motores activos a la vez sobre andes-cargo — algo que ningún ejercicio anterior probó, y que este proyecto documenta con evidencia real, sin adivinar qué pasaría. Segundo, construye un gate real —trivy image como paso obligatorio antes de kind load docker-image— y lo corre contra la propia imagen de Andes Cargo, con el resultado honesto que esa imagen produce hoy: falla.

Conexión con el módulo

Todo lo que sigue usa exclusivamente piezas ya construidas en este módulo: el Constraint de Gatekeeper (lección 4), la ClusterPolicy de Kyverno (lección 6), y los hallazgos reales de trivy image (lección 7) — este proyecto no introduce ningún concepto nuevo, solo los combina y observa qué pasa cuando conviven.


Paso 1 — Reactiva Gatekeeper: los dos motores, activos a la vez

kubectl patch k8srequiredresources andes-cargo-must-have-resource-limits \
  --type merge -p '{"spec":{"enforcementAction":"deny"}}'
kubectl get k8srequiredresources andes-cargo-must-have-resource-limits
kubectl get clusterpolicy andes-cargo-require-resource-limits

Qué esperar:

k8srequiredresources.constraints.gatekeeper.sh/andes-cargo-must-have-resource-limits patched
NAME                                    ENFORCEMENT-ACTION   TOTAL-VIOLATIONS
andes-cargo-must-have-resource-limits   deny                 0

NAME                                  ADMISSION   BACKGROUND   READY   AGE     MESSAGE
andes-cargo-require-resource-limits   true        true         True    ...     Ready

A partir de este momento, cualquier Pod que intente crearse en andes-cargo atraviesa dos ValidatingWebhookConfiguration distintas en la misma fase de admission — la de Gatekeeper (gatekeeper-validating-webhook-configuration) y la de Kyverno (kyverno-resource-validating-webhook-cfg), las dos evaluando esencialmente la misma regla, con la misma exigencia sobre resources.limits/requests.

kubectl get validatingwebhookconfigurations

Qué esperar:

NAME                                            WEBHOOKS   AGE
gatekeeper-validating-webhook-configuration     2          ...
ingress-nginx-admission                         1          ...
kyverno-cel-exception-validating-webhook-cfg    1          ...
kyverno-cleanup-validating-webhook-cfg          1          ...
kyverno-exception-validating-webhook-cfg        1          ...
kyverno-global-context-validating-webhook-cfg   1          ...
kyverno-policy-validating-webhook-cfg           1          ...
kyverno-resource-validating-webhook-cfg         1          ...
kyverno-ttl-validating-webhook-cfg              1          ...

Fíjate en el orden: gatekeeper-validating-webhook-configuration aparece antes que cualquier kyverno-*, alfabéticamente (g antes que i, antes que k). Guarda esta observación — vas a necesitarla para explicar el resultado del Paso 2.


Paso 2 — Un Pod que viola, con los dos motores activos: probado dos veces

echo "=== intento 1 ==="
kubectl apply -f bad-pod-no-limits.yaml
echo
echo "=== intento 2 ==="
kubectl apply -f bad-pod-no-limits.yaml

Qué esperar (literal — los dos intentos, corridos uno después del otro, con exactamente el mismo resultado):

=== intento 1 ===
Error from server (Forbidden): error when creating "bad-pod-no-limits.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [andes-cargo-must-have-resource-limits] container <bad-pod-no-limits> is missing required resource limits: {"cpu", "memory"}
[andes-cargo-must-have-resource-limits] container <bad-pod-no-limits> is missing required resource requests: {"cpu", "memory"}

=== intento 2 ===
Error from server (Forbidden): error when creating "bad-pod-no-limits.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [andes-cargo-must-have-resource-limits] container <bad-pod-no-limits> is missing required resource limits: {"cpu", "memory"}
[andes-cargo-must-have-resource-limits] container <bad-pod-no-limits> is missing required resource requests: {"cpu", "memory"}

El Pod fue rechazado las dos veces, de forma consistente — eso confirma que el guardrail funciona. Pero fíjate en algo más específico: en los dos intentos, kubectl solo te muestra el mensaje de Gatekeeper (validation.gatekeeper.sh). Ni una sola vez apareció el mensaje de Kyverno (validate.kyverno.svc-fail) que viste en la lección 6. La pregunta honesta que este proyecto tiene que responder: ¿significa esto que Kyverno no llegó a evaluar el Pod, o que sí lo evaluó y su respuesta simplemente no se mostró?


Paso 3 — La prueba real: ¿Kyverno evaluó el Pod, aunque su mensaje no apareciera?

Kubernetes invoca todos los ValidatingWebhookConfiguration que hacen match sobre un objeto — no se detiene en el primero que responde. Pero cuando varios webhooks deniegan la misma solicitud, kube-apiserver no está obligado a mostrarte los mensajes de todos; en la práctica de este clúster, mostró solo el primero que procesó. Los logs del propio kyverno-admission-controller, sin embargo, no dependen de qué le haya mostrado kube-apiserver a tu terminal — registran lo que Kyverno mismo hizo:

kubectl -n kyverno logs deploy/kyverno-admission-controller --since=5m | \
  grep "bad-pod-no-limits"

Qué esperar (literal — recortado a los campos que importan; el log completo de Kyverno incluye más metadata por línea):

... validation failed ... failed rules=["require-resource-requests-and-limits"] ... kind=Pod ... name=bad-pod-no-limits namespace=andes-cargo operation=CREATE ... policy=andes-cargo-require-resource-limits ...
... blocking admission request ... action=validate ... kind=Pod ... name=bad-pod-no-limits namespace=andes-cargo operation=CREATE ... policy=andes-cargo-require-resource-limits ...

Esta es la prueba directa: aparece dos veces —una por cada intento del Paso 2, con timestamps distintos—, cada vez con validation failed seguido de blocking admission request. Kyverno evaluó el Pod, encontró la violación, y decidió bloquearlo, de forma completamente independiente de Gatekeeper — su decisión, sencillamente, nunca llegó a mostrarse en tu terminal, porque el mensaje que kubectl imprimió fue el de Gatekeeper.


El hallazgo real: cómo interactúan Gatekeeper y Kyverno cuando coexisten

        kubectl apply -f bad-pod-no-limits.yaml
                        │
                        ▼
              ┌───────────────────┐
              │   kube-apiserver     │
              │   fase 3: admission   │
              └─────────┬─────────┘
                        │  invoca TODOS los ValidatingWebhookConfiguration
                        │  que hacen match (no se detiene en el primero)
              ┌─────────┴─────────┐
              ▼                   ▼
   gatekeeper-validating-   kyverno-resource-
   webhook-configuration    validating-webhook-cfg
   (evalúa, deniega)        (evalúa, deniega —
              │              confirmado en logs)
              │                   │
              └─────────┬─────────┘
                        │  AMBOS respondieron allowed: false
                        ▼
              kube-apiserver rechaza el request
              (muestra SOLO el mensaje de Gatekeeper,
               el primero en el orden alfabético de nombres
               de ValidatingWebhookConfiguration observado
               en este clúster — "gatekeeper-..." antes que
               "kyverno-...")

Tres conclusiones honestas, verificadas dos veces en este laboratorio, ninguna inventada:

  1. No hubo ningún conflicto funcional entre los dos motores. Los dos evaluaron el mismo objeto, los dos llegaron a la misma conclusión (rechazar), y el resultado neto para el clúster —el Pod nunca llegó a etcd— fue idéntico al de cada motor por separado. "Chocar" en el sentido de que un motor deshiciera o contradijera al otro, no ocurrió.
  2. Sí hubo una pérdida de información para el usuario. kubectl solo mostró un mensaje, el de Gatekeeper — sin revisar los logs del Paso 3, hubiera sido imposible confirmar, desde la terminal, que Kyverno también evaluó y también rechazó. En un clúster real con dos motores activos y políticas superpuestas, esto es exactamente el tipo de ambigüedad operativa que la lección 5 de este módulo nombró como una razón real para que la mayoría de los equipos termine estandarizando en un solo motor, no una limitación técnica que impida que convivan.
  3. El orden observado (Gatekeeper antes que Kyverno) coincidió, en este clúster, con el orden alfabético de los nombres de ValidatingWebhookConfiguration (gatekeeper-validating-webhook-configuration antes que kyverno-resource-validating-webhook-cfg) — pero la documentación oficial de Kubernetes no garantiza ningún orden específico de invocación entre ValidatingWebhookConfiguration distintas. Esta guía reporta lo que observó, dos veces, de forma consistente, en este clúster específico — no lo presenta como una regla garantizada por la API de Kubernetes para cualquier clúster.

Paso 4 — El gate de imagen: trivy image antes de kind load docker-image

La segunda mitad de este proyecto convierte el hallazgo de la lección 7 en un guardrail operativo real: un script que escanea una imagen con trivy image antes de cargarla en el clúster, y aborta si encuentra hallazgos CRITICAL.

mkdir -p scripts
cat > scripts/scan-and-load.sh << 'EOF'
#!/usr/bin/env bash
# scan-and-load.sh — trivy image gate before kind load docker-image
# Usage: ./scan-and-load.sh <image:tag>
set -euo pipefail

IMAGE="$1"
CLUSTER="andes-cargo-cluster"

echo "==> scanning ${IMAGE} for CRITICAL vulnerabilities before loading into ${CLUSTER}"

if trivy image --exit-code 1 --severity CRITICAL --quiet "${IMAGE}"; then
  echo "==> gate PASSED — no CRITICAL findings, loading ${IMAGE} into ${CLUSTER}"
  kind load docker-image "${IMAGE}" --name "${CLUSTER}"
  echo "==> ${IMAGE} loaded"
else
  echo "==> gate FAILED — ${IMAGE} has CRITICAL findings, NOT loaded into ${CLUSTER}"
  exit 1
fi
EOF
chmod +x scripts/scan-and-load.sh

El script no inventa ninguna capacidad nueva de Trivy — encadena dos comandos que ya conoces (trivy image --exit-code 1 --severity CRITICAL, que la lección 7 no usó todavía, y kind load docker-image, del Módulo 1) con lógica de shell simple: si el escaneo falla (hay al menos un CRITICAL), el script nunca llega a la línea de kind load.

Corrida 1 — contra andes-cargo-status-api:latest: el gate falla, de verdad

./scripts/scan-and-load.sh andes-cargo-status-api:latest
echo "exit code: $?"

Qué esperar (literal — recortado a las filas relevantes; el reporte completo de Trivy es el mismo que la lección 7 ya mostró completo):

==> scanning andes-cargo-status-api:latest for CRITICAL vulnerabilities before loading into andes-cargo-cluster

andes-cargo-status-api:latest (debian 13.6)
===========================================
Total: 4 (CRITICAL: 4)

┌───────────┬────────────────┬──────────┬──────────────┬───────────────────┬───────────────┐
│  Library  │ Vulnerability  │ Severity │    Status    │ Installed Version │ Fixed Version │
├───────────┼────────────────┼──────────┼──────────────┼───────────────────┼───────────────┤
│ perl-base │ CVE-2026-13221 │ CRITICAL │ affected     │ 5.40.1-6          │               │
│           │ CVE-2026-42496 │          │ fix_deferred │                   │               │
│           │ CVE-2026-57433 │          │ affected     │                   │               │
│           │ CVE-2026-8376  │          │              │                   │               │
└───────────┴────────────────┴──────────┴──────────────┴───────────────────┴───────────────┘
==> gate FAILED — andes-cargo-status-api:latest has CRITICAL findings, NOT loaded into andes-cargo-cluster
exit code: 1

Esto es una honestidad incómoda a propósito, no un accidente del laboratorio: la propia imagen de producción de Andes Cargo, corriendo en andes-cargo-cluster desde el Módulo 1, no pasaría hoy este gate si lo aplicaras de forma estricta — los mismos cuatro CRITICAL de perl-base que la lección 7 encontró, sin Fixed Version disponible todavía. Un equipo real, frente a este resultado exacto, tiene tres caminos honestos —el mismo patrón que cloud-security-and-guardrails-guide ya estableció—: corregir (no aplica aquí, no hay parche disponible todavía), suprimir con justificación documentada (marcar estos cuatro CVE específicos como aceptados, con fecha de revisión, en un archivo .trivyignore — una decisión de política, no técnica), o aceptar el riesgo explícitamente y ajustar el umbral del gate (por ejemplo, bloquear solo si aparece un CRITICAL nuevo desde la última revisión, no cualquier CRITICAL sin excepción). Este proyecto no toma esa decisión por ti — la deja documentada, con el gate funcionando exactamente como debería: deteniendo, con evidencia real, antes de que nadie tenga que descubrirlo en producción.

Corrida 2 — contra una imagen limpia: el gate pasa, y carga de verdad

docker pull hello-world:latest
./scripts/scan-and-load.sh hello-world:latest
echo "exit code: $?"

Qué esperar:

==> scanning hello-world:latest for CRITICAL vulnerabilities before loading into andes-cargo-cluster
==> gate PASSED — no CRITICAL findings, loading hello-world:latest into andes-cargo-cluster
Image: "hello-world:latest" with ID "sha256:eb84fdc6..." not yet present on node "andes-cargo-cluster-worker", loading...
Image: "hello-world:latest" with ID "sha256:eb84fdc6..." not yet present on node "andes-cargo-cluster-control-plane", loading...
Image: "hello-world:latest" with ID "sha256:eb84fdc6..." not yet present on node "andes-cargo-cluster-worker2", loading...
==> hello-world:latest loaded
exit code: 0

hello-world:latest no tiene sistema operativo Debian/Alpine detrás —es, deliberadamente, la imagen más mínima que existe en Docker Hub—, así que Trivy no encuentra ningún paquete que escanear, y el gate pasa limpio. El sha256 es variable (depende de la versión exacta que Docker Hub sirva); el patrón de las tres líneas "not yet present... loading..." —una por cada nodo del clúster— es fijo, el mismo mecanismo que el Módulo 1 ya usó para andes-cargo-status-api:latest.


Evidencia adicional: lo que el clúster real ya cumple

Antes de cerrar el proyecto, vale la pena confirmar algo que las dos secciones anteriores dieron por sentado: el Deployment andes-cargo-status-api real —el que corre en producción desde el Módulo 1, con los límites del Módulo 3— sigue pasando ambas políticas, sin ningún cambio de tu parte en este módulo.

kubectl get policyreport -n andes-cargo

Qué esperar:

NAME                                   KIND         NAME                                      PASS   FAIL   WARN   ERROR   SKIP   AGE
417220d1-3f46-476c-a6f8-ac1a5e501853   Pod          andes-cargo-status-api-548966dd97-xx6zs   1      0      0      0       0      ...
6097b68c-4f8f-41c7-8875-31aab943cb5d   Pod          andes-cargo-status-api-548966dd97-gsgx8   1      0      0      0       0      ...
c3058c00-f825-43c8-b68c-e03e608840fd   Deployment   andes-cargo-status-api                    1      0      0      0       0      ...
debf13b0-c282-446c-a1b8-184a60c47e92   ReplicaSet   andes-cargo-status-api-548966dd97         1      0      0      0       0      ...

PASS: 1, FAIL: 0 en las cuatro filas — es Kyverno (kyverno-reports-controller, en segundo plano) confirmando lo mismo que TOTAL-VIOLATIONS: 0 de Gatekeeper ya mostró desde la lección 4: el guardrail de este módulo no tuvo que corregir nada porque el Módulo 3 ya lo hizo bien. Los sufijos hash de cada NAME son variables.


Checklist final del Módulo 6

PiezaMotor/herramientaEstado
Rechazo de un Pod sin límites de recursosGatekeeper v3.23.0, aisladoEjecutado (lección 4) — mensaje literal capturado
Rechazo de un Pod sin límites de recursosKyverno v1.18.2, aisladoEjecutado (lección 6) — mensaje literal capturado
Rechazo de un Pod sin límites de recursosGatekeeper + Kyverno, ambos activosEjecutado, dos veces (este proyecto) — mensaje de Gatekeeper en kubectl, evaluación de Kyverno confirmada en logs
Admisión de un Pod con límites de recursosGatekeeper y Kyverno, cada uno por separadoEjecutado (lecciones 4 y 6) — 1/1 Running en ambos casos
Escaneo de vulnerabilidades de imagentrivy imageEjecutado (lección 7) — 184 hallazgos reales, 4 CRITICAL sin parche disponible
Gate de imagen antes de kind load docker-imagetrivy image --exit-code 1 --severity CRITICAL + scripts/scan-and-load.shEjecutado, dos veces — falla real contra andes-cargo-status-api:latest, pasa real contra hello-world:latest
Confirmación de que el Deployment real cumple ambas políticasPolicyReport de Kyverno + TOTAL-VIOLATIONS de GatekeeperEjecutadoPASS: 1, FAIL: 0 en las cuatro filas del namespace

Limpieza: deja el clúster en un estado consistente

kubectl delete pod bad-pod-no-limits -n andes-cargo --ignore-not-found
docker rmi hello-world:latest 2>/dev/null || true

El primer comando confirma que ningún Pod de prueba quedó corriendo (los dos intentos del Paso 2 fueron rechazados, así que en teoría no debería haber nada que borrar — --ignore-not-found evita un error si ese es el caso). El segundo es opcional, solo para no dejar una imagen de prueba sin uso en tu Docker local.


Errores comunes

Interpretar el hallazgo del Paso 3 como "Gatekeeper y Kyverno están en conflicto" (de lenguaje impreciso). Qué pasa: alguien, al ver que solo un mensaje aparece en kubectl mientras los dos motores evaluaron, concluye que "chocan" en el sentido de romperse o contradecirse. Cómo detectarlo: si tu resumen de este proyecto es "Gatekeeper y Kyverno no son compatibles entre sí". Cómo corregirlo: repasa la sección "El hallazgo real" de esta lección — los dos motores llegaron a la misma conclusión, de forma completamente independiente, y el resultado neto (el Pod nunca existió) fue idéntico. Lo único que "se pierde" es visibilidad para el humano que lee la terminal, no corrección del guardrail — el clúster está protegido igual de bien con uno o con los dos motores activos.

Asumir que el gate --severity CRITICAL de este proyecto es la única forma válida de definir un umbral (de rigidez innecesaria). Qué pasa: alguien copia scan-and-load.sh literalmente y asume que "bloquear en cualquier CRITICAL, sin excepción" es la única política razonable para cualquier equipo. Cómo detectarlo: si tu reacción, después de ver la Corrida 1 fallar contra la propia imagen de Andes Cargo, es "entonces nunca se podría desplegar nada". Cómo corregirlo: el umbral (--severity CRITICAL, sin excepciones) es una decisión de diseño de este proyecto específico, elegida a propósito para que veas un resultado real, no artificial. Un equipo real, frente al mismo resultado, normalmente ajustaría el gate —excluir CVE específicos ya revisados y aceptados, con .trivyignore y fecha de revisión, o bloquear solo hallazgos nuevos desde la última auditoría— en vez de bloquear indefinidamente un servicio que de otra forma funciona bien.

No revisar los logs de Kyverno del Paso 3, y quedarte con la duda de si realmente evaluó el Pod (de verificación incompleta). Qué pasa: alguien lee el Paso 2, ve solo el mensaje de Gatekeeper, y sigue adelante sin correr el Paso 3 — quedándose, sin saberlo, sin la evidencia que confirma o refuta si Kyverno participó. Cómo detectarlo: si terminaste este proyecto sin haber visto, con tus propios ojos, las líneas validation failed/blocking admission request en los logs de kyverno-admission-controller. Cómo corregirlo: corre el comando del Paso 3 antes de dar por cerrado este proyecto — es la única forma de convertir "probablemente los dos evaluaron" en "confirmado, con timestamps, que los dos evaluaron".


Ejercicios

Ejercicio 1 — Diseña una prueba que confirme el orden observado con un tercer motor hipotético. Si instalaras un tercer motor de admission control hipotético, con un ValidatingWebhookConfiguration llamado aaa-first-policy-engine, ¿qué esperarías ver en el mensaje de rechazo de un Pod que las tres políticas rechazan a la vez, basándote en el patrón que observaste en este proyecto?

Ver solución

Basándote en el patrón observado (Gatekeeper, con nombre gatekeeper-..., apareció consistentemente antes que Kyverno, con nombre kyverno-..., en los dos intentos del Paso 2), sería razonable predecir que aaa-first-policy-engine —alfabéticamente antes que gatekeeper- y que kyverno-— aparecería como el mensaje mostrado en kubectl. Pero la respuesta completa y honesta debe incluir la advertencia de esta lección: este proyecto observó ese orden de forma consistente en este clúster específico, sin que la documentación oficial de Kubernetes garantice ese comportamiento para cualquier clúster o cualquier versión de kube-apiserver — la única forma de confirmarlo con certeza sería, como en el Paso 3, revisar los logs de cada motor por separado, no confiar únicamente en qué mensaje kubectl decide mostrar.

Ejercicio 2 — Explica la decisión de umbral del gate a un compañero que pregunta "¿por qué no bloqueamos también los HIGH?". Un compañero de equipo, viendo que scan-and-load.sh solo bloquea en CRITICAL, pregunta por qué no incluye también HIGH —después de todo, la lección 7 encontró 21 hallazgos HIGH reales—. ¿Qué le responderías?

Ver solución

Una respuesta razonable: "Es una decisión de umbral, no una limitación técnica del script — cambiar --severity CRITICAL a --severity CRITICAL,HIGH es un solo flag. La razón para empezar con CRITICAL únicamente es demostrarnos, con este mismo laboratorio, qué tan estricto podemos ponernos sin bloquear indefinidamente nuestra propia imagen de producción: ya vimos que hasta con el umbral más permisivo (solo CRITICAL), la imagen actual falla el gate. Subir a HIGH agregaría 21 hallazgos más a esa lista, la mayoría en el sistema operativo base, sin ningún cambio de nuestra parte que los resuelva de inmediato. Antes de subir el umbral, necesitaríamos revisar esos 21 uno por uno y decidir cuáles aceptamos — el mismo proceso de fix/suppress/accept que ya usamos con los CRITICAL, aplicado a una lista más larga."

Ejercicio 3 — Predice qué pasaría si borraras el Constraint de Gatekeeper pero dejaras la ClusterPolicy de Kyverno activa, y repitieras el Paso 2. Sin ejecutar nada, predice: si corrieras kubectl delete k8srequiredresources andes-cargo-must-have-resource-limits y después repitieras el intento de crear bad-pod-no-limits.yaml, ¿qué mensaje esperarías ver, y en qué cambiaría respecto al Paso 2 de este proyecto?

Ver solución

El Pod seguiría siendo rechazado —Kyverno, con su ClusterPolicy todavía activa, es completamente independiente de que el Constraint de Gatekeeper exista o no—, pero el mensaje que kubectl mostraría sería, esta vez, el de Kyverno (admission webhook "validate.kyverno.svc-fail" denied the request..., el mismo texto exacto que viste en la lección 6), porque ya no habría ningún otro webhook compitiendo por aparecer primero. Este experimento, si lo corres de verdad, es la forma más directa de confirmar que los dos motores actúan de forma genuinamente independiente: quitar uno no cambia en nada la protección que el otro sigue ofreciendo.


Resumen y siguiente paso

Este proyecto activó Gatekeeper y Kyverno a la vez sobre andes-cargo, y documentó, con evidencia real (dos intentos idénticos, más los logs de Kyverno), que los dos motores evalúan cada objeto de forma independiente y llegan a la misma conclusión — sin ningún conflicto funcional, aunque kubectl solo muestre el mensaje de uno de los dos. Construyó, además, un gate real de trivy image antes de kind load docker-image, y lo corrió dos veces: falló, honestamente, contra la propia imagen de producción de Andes Cargo (cuatro CRITICAL sin parche disponible, heredados de perl-base), y pasó, cargando de verdad, contra una imagen sin sistema operativo detrás. andes-cargo-cluster termina este módulo con un portero real, doble, en su única puerta de entrada — y con la honestidad completa de qué haría ese portero si lo pusieras hoy mismo a evaluar su propia carga de trabajo.

Antes de avanzar deberías poder: explicar, sin usar la palabra "conflicto", qué pasa cuando dos ValidatingWebhookConfiguration evalúan el mismo objeto; reproducir el gate de trivy image antes de cualquier kind load docker-image futuro; y nombrar los tres caminos honestos frente a un hallazgo CRITICAL sin parche disponible (corregir, suprimir con justificación, o aceptar el riesgo explícitamente).

Siguiente módulo: lo específico de EKS en producción. El Módulo 7 toma todo lo construido en kind —Pods, Services, GitOps, y ahora estos guardrails de runtime— y confronta la pregunta que ningún módulo anterior pudo responder con un clúster local: qué cambia, de verdad, cuando el plano de control ya no lo administras tú, sino AWS.

Recursos

  1. Kubernetes — Dynamic Admission Control — comportamiento oficial de invocación de múltiples ValidatingWebhookConfiguration sobre el mismo objeto.
  2. Gatekeeper — Violations y Kyverno — Policy Reports — las dos fuentes de evidencia que este proyecto cruzó para confirmar que ambos motores evaluaron de forma independiente.
  3. Trivy — Container Image — referencia de --exit-code/--severity, los dos flags que convierten un escaneo informativo en un gate real.
  4. kind — Load a local image into your cluster — documentación oficial de kind load docker-image, el comando que este proyecto pone detrás del gate.
  5. cloud-security-and-guardrails-guide (NIEVA), Módulo 5, lección 6 — el patrón de "fix, suppress, o accept" que este proyecto retoma frente a los cuatro CRITICAL sin parche disponible.