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

6. Manos a la obra: la misma política, en Kyverno

Descripción

Esta lección instala Kyverno v1.18.2 de verdad contra andes-cargo-cluster —el mismo clúster donde Gatekeeper sigue corriendo desde la lección 4—, aplica la ClusterPolicy de la lección 5, y repite exactamente la misma prueba: un Pod que viola, un Pod que cumple. Antes de instalar nada, esta lección toma una decisión operativa explícita —y la explica— para que la prueba de Kyverno no quede confundida por el Constraint de Gatekeeper que ya está activo.

Conexión con el módulo

Gatekeeper y Kyverno, si los dos tienen enforcementAction/validationFailureAction en modo bloqueo a la vez, y los dos hacen match sobre el mismo namespace con la misma regla, van a rechazar el mismo Pod los dos, en paralelo. Eso es exactamente lo que la lección 8 de este módulo (el proyecto) va a probar a propósito, con evidencia completa de cómo interactúan. Esta lección, en cambio, necesita una prueba aislada de Kyverno — así que, antes de instalar nada, el primer paso es apagar temporalmente la aplicación del Constraint de Gatekeeper, sin borrarlo.


Paso 1 — Aísla la prueba: pon el Constraint de Gatekeeper en dryrun

kubectl patch k8srequiredresources andes-cargo-must-have-resource-limits \
  --type merge -p '{"spec":{"enforcementAction":"dryrun"}}'
kubectl get k8srequiredresources andes-cargo-must-have-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   dryrun               0

dryrun significa que Gatekeeper sigue evaluando cada objeto contra la regla —vas a poder confirmarlo con TOTAL-VIOLATIONS si aplicas algo que la viole— pero ya no bloquea nada: cualquier Pod, cumpla o no, llega a etcd igual. Este es el modo exacto que la lección 4 nombró y no usó todavía. La razón de este paso, dicha sin rodeos: si dejaras a Gatekeeper en deny mientras pruebas Kyverno, el Pod de prueba sin límites sería rechazado por el motor equivocado —Gatekeeper respondería primero, y nunca sabrías si la ClusterPolicy de Kyverno, por sí sola, también lo hubiera rechazado—. Aislar la prueba es lo que te permite atribuir el resultado al motor correcto.


Paso 2 — Instala Kyverno v1.18.2

curl -sL -o kyverno-install.yaml \
  https://github.com/kyverno/kyverno/releases/download/v1.18.2/install.yaml
kubectl create -f kyverno-install.yaml

Fíjate que este comando usa kubectl create, no kubectl apply —a diferencia del manifiesto de Gatekeeper—. La razón es puramente técnica: el manifiesto de Kyverno incluye definiciones de CRD con esquemas extensos (openAPIV3Schema completos, para validar cada campo de una ClusterPolicy), y kubectl apply guarda una copia completa del objeto anterior como anotación para poder calcular diferencias en el próximo apply — con esquemas de este tamaño, esa anotación puede superar el límite de tamaño que Kubernetes permite para una anotación individual. kubectl create evita el problema por completo: crea los objetos directamente, sin guardar ese historial de "última configuración aplicada". Es la misma instalación, documentada así por el propio proyecto Kyverno.

Qué esperar (fijo — el manifiesto oficial no cambia la lista de recursos que crea):

namespace/kyverno created
serviceaccount/kyverno-admission-controller created
configmap/kyverno created
customresourcedefinition.apiextensions.k8s.io/clusterpolicies.kyverno.io created
customresourcedefinition.apiextensions.k8s.io/policies.kyverno.io created
customresourcedefinition.apiextensions.k8s.io/policyreports.wgpolicyk8s.io created
...
clusterrole.rbac.authorization.k8s.io/kyverno:admission-controller created
service/kyverno-svc created
deployment.apps/kyverno-admission-controller created
deployment.apps/kyverno-background-controller created
deployment.apps/kyverno-cleanup-controller created
deployment.apps/kyverno-reports-controller created

Cuatro Deployment, no uno —una diferencia real de arquitectura frente a Gatekeeper, que instaló dos (gatekeeper-controller-manager y gatekeeper-audit)—: kyverno-admission-controller (el que responde al ValidatingWebhookConfiguration, el equivalente directo de gatekeeper-controller-manager), kyverno-background-controller (re-evalúa objetos existentes, el equivalente de gatekeeper-audit), kyverno-cleanup-controller (borra recursos según políticas de limpieza programadas, sin equivalente en este módulo), y kyverno-reports-controller (genera los PolicyReport que vas a ver más adelante en este módulo).

Espera a que el controlador de admission esté listo:

kubectl -n kyverno rollout status deployment/kyverno-admission-controller --timeout=180s
kubectl -n kyverno get pods

Qué esperar:

deployment "kyverno-admission-controller" successfully rolled out
NAME                                             READY   STATUS    RESTARTS   AGE
kyverno-admission-controller-656b594944-vzv79    1/1     Running   0          30s
kyverno-background-controller-59bd999b84-rc84p   1/1     Running   0          30s
kyverno-cleanup-controller-657f9dd6d5-pt5lx      1/1     Running   0          30s
kyverno-reports-controller-746c796cf8-l7sc4      1/1     Running   0          30s

Sufijos hash y AGE variables, como siempre.


Paso 3 — La ClusterPolicy: la política equivalente, en YAML

mkdir -p kyverno
cat > kyverno/policy-andes-cargo-require-resources.yaml << 'EOF'
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: andes-cargo-require-resource-limits
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: require-resource-requests-and-limits
      match:
        any:
          - resources:
              kinds:
                - Pod
              namespaces:
                - andes-cargo
      validate:
        message: "every container must set resources.requests and resources.limits for cpu and memory"
        pattern:
          spec:
            containers:
              - resources:
                  requests:
                    cpu: "?*"
                    memory: "?*"
                  limits:
                    cpu: "?*"
                    memory: "?*"
EOF

kubectl apply -f kyverno/policy-andes-cargo-require-resources.yaml
kubectl get clusterpolicy andes-cargo-require-resource-limits

Qué esperar:

clusterpolicy.kyverno.io/andes-cargo-require-resource-limits created
NAME                                  ADMISSION   BACKGROUND   READY   AGE   MESSAGE
andes-cargo-require-resource-limits   true        true         True    3s    Ready

ADMISSION: true confirma que esta política evalúa en tiempo real, en el momento del kubectl apply de cualquier Pod nuevo — el mismo instante que la fase 3 de la lección 2 describió. BACKGROUND: true (heredado de spec.background: true en el YAML) confirma que, además, kyverno-background-controller va a re-evaluar objetos ya existentes, el equivalente de gatekeeper-audit. READY: True confirma que Kyverno terminó de registrar la política sin ningún error de validación de su propio esquema.


Paso 4 — El Pod que la viola: rechazado (con Kyverno como único motor activo)

kubectl apply -f bad-pod-no-limits.yaml

Qué esperar (literal — mensaje de rechazo real de Kyverno, con Gatekeeper en dryrun y por lo tanto sin interferir):

Error from server: error when creating "bad-pod-no-limits.yaml": admission webhook "validate.kyverno.svc-fail" denied the request:

resource Pod/andes-cargo/bad-pod-no-limits was blocked due to the following policies

andes-cargo-require-resource-limits:
  require-resource-requests-and-limits: 'validation error: every container must set
    resources.requests and resources.limits for cpu and memory. rule require-resource-requests-and-limits
    failed at path /spec/containers/0/resources/limits/'

Compara este mensaje con el de Gatekeeper de la lección 4, y fíjate en las diferencias reales de formato:

  • admission webhook "validate.kyverno.svc-fail" — un nombre de webhook completamente distinto al "validation.gatekeeper.sh" de la lección 4, aunque los dos cumplen exactamente el mismo rol dentro de la fase 3 del ciclo de vida.
  • 'validation error: every container must set resources.requests and resources.limits...' — este es, literalmente, el texto que pusiste en spec.rules[].validate.message del YAML del Paso 3 — a diferencia del mensaje de Gatekeeper (generado dinámicamente por la función sprintf dentro de Rego), el mensaje de Kyverno es, casi siempre, el texto fijo que tú mismo escribiste.
  • failed at path /spec/containers/0/resources/limits/ — Kyverno te da la ruta exacta, dentro del YAML del objeto, donde el pattern no coincidió — información que Gatekeeper, en este ejemplo, te dio distinto (qué campos faltan, {"cpu", "memory"}), no dónde exactamente en la estructura.

Y, exactamente igual que con Gatekeeper: kubectl get pods -n andes-cargo | grep bad-pod no muestra nada. El objeto nunca existió.


Paso 5 — El Pod que la cumple: admitido

kubectl apply -f good-pod-with-limits.yaml
kubectl get pod good-pod-with-limits -n andes-cargo

Qué esperar:

pod/good-pod-with-limits created
NAME                   READY   STATUS    RESTARTS   AGE
good-pod-with-limits   1/1     Running   0           5s

El mismo YAML exacto de la lección 4, evaluado ahora por un motor completamente distinto, con el mismo resultado: 1/1 Running. Limpia el Pod antes de seguir:

kubectl delete pod good-pod-with-limits -n andes-cargo

Comparación lado a lado: los dos motores, el mismo resultado

Gatekeeper (lección 4)Kyverno (esta lección)
Objetos necesarios2 (ConstraintTemplate + Constraint)1 (ClusterPolicy)
Lenguaje de la reglaRego (violation[...] { ... })YAML (pattern, operador "?*")
Nombre del webhook en el rechazovalidation.gatekeeper.shvalidate.kyverno.svc-fail
Interruptor bloqueo/auditoríaspec.enforcementAction (deny/dryrun/warn)spec.validationFailureAction (Enforce/Audit)
Mensaje de rechazoGenerado dinámicamente por la lógica RegoEl texto fijo de validate.message, casi siempre
Deployment de admissiongatekeeper-controller-manager (3 réplicas)kyverno-admission-controller
Deployment de re-evaluación en segundo planogatekeeper-auditkyverno-background-controller
Pod sin límites (bad-pod-no-limits)Rechazado, nunca llega a etcdRechazado, nunca llega a etcd
Pod con límites (good-pod-with-limits)Admitido, 1/1 RunningAdmitido, 1/1 Running

La última fila de esa tabla es la conclusión real de esta lección: para el resultado que le importa al usuario del clúster, los dos motores son indistinguibles. La diferencia está enteramente en cómo cada equipo, del lado de quien escribe la política, prefiere expresarla — no en qué tan bien protegen el clúster.


Errores comunes

Olvidar el Paso 1 y quedarte confundido sobre cuál motor rechazó el Pod (de aislamiento de prueba). Qué pasa: alguien salta directo al Paso 4 sin poner a Gatekeeper en dryrun, y el mensaje de rechazo muestra validation.gatekeeper.sh, no validate.kyverno.svc-fail — parece que Kyverno "no hizo nada". Cómo detectarlo: si el nombre del webhook en tu mensaje de error no coincide con el que esta lección muestra. Cómo corregirlo: confirma con kubectl get k8srequiredresources andes-cargo-must-have-resource-limits que ENFORCEMENT-ACTION diga dryrun, no deny. Si sigue en deny, repetí el kubectl patch del Paso 1 — sin ese paso, Gatekeeper (que ya estaba activo desde la lección 4) va a responder primero, y el mensaje que veas va a ser el suyo, no el de Kyverno. Esto no es un error de configuración de Kyverno; es exactamente el fenómeno de interacción entre dos motores que la lección 8 de este módulo documenta a propósito.

Usar kubectl apply en vez de kubectl create para instalar Kyverno, y toparte con un error de tamaño de anotación (de instalación). Qué pasa: kubectl apply -f kyverno-install.yaml falla con un mensaje sobre metadata.annotations: Too long: must have at most 262144 bytes, o un error similar sobre el tamaño de last-applied-configuration. Cómo detectarlo: el error menciona explícitamente el límite de tamaño de una anotación. Cómo corregirlo: usa kubectl create -f kyverno-install.yaml, como el Paso 2 de esta lección — evita por completo el mecanismo de anotación que causa el problema. Si ya instalaste Kyverno con apply y falló a mitad de camino, corre kubectl delete -f kyverno-install.yaml antes de reintentar con create, para no dejar recursos parciales.

Confundir background: true con "esta política corre solo en segundo plano, nunca en admisión" (de lectura incompleta del campo). Qué pasa: alguien lee spec.background: true en el YAML del Paso 3 y asume que la política no bloquea nada en tiempo real. Cómo detectarlo: si tu expectativa, antes del Paso 4, era que bad-pod-no-limits.yaml se creara sin problema. Cómo corregirlo: background: true es aditivo, no exclusivo — significa "además de evaluar en el momento del apply (comportamiento por defecto de cualquier regla validate), también re-evalúa objetos que ya existen, periódicamente, en segundo plano". El ADMISSION: true que viste en la salida del Paso 3 es el campo que confirma que la evaluación en tiempo real sigue activa; background no la reemplaza, la complementa.


Ejercicios

Ejercicio 1 — Predice el READY/MESSAGE de una ClusterPolicy con un YAML mal formado. Sin ejecutar nada, predice: si el pattern del Paso 3 tuviera un error de indentación YAML (por ejemplo, limits al mismo nivel que requests en vez de anidado dentro de resources), ¿qué esperarías ver en la columna READY de kubectl get clusterpolicy?

Ver solución

Dependería del tipo exacto de error: un YAML sintácticamente inválido (indentación rota de forma que ni siquiera parsea como YAML válido) haría que kubectl apply fallara de inmediato, sin llegar a crear la ClusterPolicy en absoluto — el error aparecería en tu propia terminal, no en el estado del objeto. Un YAML sintácticamente válido pero semánticamente incorrecto para el esquema de Kyverno (por ejemplo, un campo con un tipo equivocado) sí crearía el objeto, pero mostraría READY: False, con un MESSAGE describiendo el problema específico de validación — el mismo patrón que ya viste con k8srequiredresources en Gatekeeper cuando su openAPIV3Schema rechaza un Constraint mal formado.

Ejercicio 2 — Explica, en una frase, por qué el mensaje de rechazo de Kyverno incluye una ruta (/spec/containers/0/resources/limits/) y el de Gatekeeper, en este ejemplo, no. Sin repetir el texto de esta lección, ¿a qué se debe esa diferencia?

Ver solución

Se debe a cómo cada motor genera su mensaje internamente, no a una limitación de uno sobre el otro: el pattern de Kyverno es, literalmente, una comparación estructural contra el árbol YAML del objeto — cuando la comparación falla, Kyverno ya sabe exactamente en qué nodo del árbol ocurrió el desajuste, y lo reporta como una ruta. La regla Rego de Gatekeeper, en este ConstraintTemplate específico, construyó su mensaje con sprintf a partir de un conjunto (missing) de nombres de campos faltantes — una decisión de cómo se escribió esa regla en particular, no una limitación del lenguaje: una regla Rego distinta podría perfectamente incluir la ruta también, si quien la escribió decidiera construir el mensaje de esa forma.

Ejercicio 3 — Diseña la prueba que confirmarías antes de reactivar Gatekeeper a deny. Antes de que la lección 8 reactive el Constraint de Gatekeeper (volviéndolo de dryrun a deny), ¿qué comando correrías para confirmar que, mientras estuvo en dryrun, Gatekeeper evaluó el Pod sin límites (aunque no lo haya bloqueado), como evidencia de que dryrun audita sin bloquear?

Ver solución

kubectl get k8srequiredresources andes-cargo-must-have-resource-limits — si en algún momento durante esta lección hubiera quedado un Pod sin límites corriendo en andes-cargo (por ejemplo, si Kyverno no lo hubiera bloqueado y no lo hubieras limpiado), la columna TOTAL-VIOLATIONS de ese Constraint mostraría un número mayor a 0, confirmando que gatekeeper-audit sí registró la violación en segundo plano, aunque dryrun nunca haya impedido que el objeto llegara a existir. En este laboratorio específico, como limpiaste cada Pod de prueba antes de seguir, TOTAL-VIOLATIONS debería seguir en 0 — la ausencia de violaciones registradas confirma que no quedó ningún recurso de prueba sin limpiar, no que Gatekeeper haya dejado de auditar.


Resumen y siguiente paso

Esta lección instaló Kyverno v1.18.2 de verdad, aisló la prueba poniendo a Gatekeeper en dryrun (documentando la razón exacta: evitar atribuir un rechazo al motor equivocado), aplicó la ClusterPolicy equivalente en YAML, y confirmó el mismo resultado que la lección 4 —Pod sin límites rechazado, Pod con límites admitido— con un motor completamente distinto por debajo. La tabla comparativa dejó fijo lo que de verdad importa: para el resultado final, Gatekeeper y Kyverno son indistinguibles; lo que cambia es únicamente cómo cada equipo prefiere escribir la regla.

Antes de avanzar deberías poder: explicar por qué la instalación de Kyverno usa kubectl create en vez de apply; leer un mensaje de rechazo de Kyverno e identificar sus tres piezas (webhook, política/regla, mensaje); y reproducir de memoria el Paso 1 (aislar la prueba) antes de instalar cualquier motor de políticas nuevo sobre un clúster que ya tiene otro activo.

Siguiente lección: manos a la obra — trivy image sobre andes-cargo-status-api. Ahí vas a cambiar de capa por completo: en vez de evaluar la forma de un objeto de Kubernetes antes de crearlo, vas a escanear el contenido de la imagen Docker que ese objeto referencia — un artefacto completamente distinto del terraform plan/HCL que cloud-security-and-guardrails-guide escaneó con la misma herramienta.

Recursos

  1. Kyverno — Installation — la fuente del manifiesto oficial v1.18.2 aplicado en esta lección, y la recomendación de kubectl create sobre kubectl apply.
  2. Kyverno — Background Scans — documentación oficial del campo background y kyverno-background-controller.
  3. Kyverno — Policy Reports — referencia del PolicyReport generado por kyverno-reports-controller, que vas a ver en la lección 8.
  4. Gatekeeper — Violations — documentación de enforcementAction, contrastado en esta lección con validationFailureAction de Kyverno.