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

5. Kyverno: la alternativa YAML-nativa

Descripción

Gatekeeper resuelve el problema de esta guía —rechazar un Pod sin límites de recursos— con dos objetos nuevos y un lenguaje nuevo (Rego). Kyverno resuelve exactamente el mismo problema con un solo objeto (ClusterPolicy) escrito enteramente en YAML — el mismo formato en el que ya escribiste cada Deployment, Service y NetworkPolicy de esta guía, sin aprender ninguna sintaxis adicional. Esta lección explica por qué existen dos motores completos para el mismo problema, y por qué esta guía no declara un ganador entre ellos.

Conexión con el módulo

La lección 6 va a instalar Kyverno v1.18.2 y escribir la política equivalente a la de la lección 4, probada con los mismos dos Pods. Esta lección, primero, deja claro qué cambia y qué no cambia entre los dos motores — porque lo que no cambia es más importante de lo que parece: los dos son admission controllers preventivos, los dos se conectan al mismo ValidatingWebhookConfiguration que la lección 2 describió, los dos evalúan el mismo AdmissionReview en el mismo instante del ciclo de vida de un request. Lo único que cambia es el lenguaje en el que le dices al motor qué revisar.


Analogía: dos idiomas para escribir la misma regla del portero

El portero de la lección 1 tiene, ahora, dos formas igual de válidas de escribir su lista de reglas. Podría escribirla en un lenguaje formal de lógica —"para todo elemento X que ingrese, si X carece del atributo Y, entonces rechazar X"— que exige aprender su gramática específica, pero que le permite expresar reglas arbitrariamente complejas, incluso combinando información de fuentes externas a la lista misma. O podría escribirla como una lista de verificación directa —"revisar que la maleta tenga: (1) una etiqueta con el destino, (2) un candado cerrado"— que cualquiera que ya sepa leer una lista de compras entiende sin entrenamiento adicional, pero que empieza a volverse difícil de leer en cuanto la regla necesita lógica más elaborada que una simple lista de "esto debe estar presente". Ninguna de las dos formas es objetivamente superior: la primera (Rego, Gatekeeper) le da al portero un lenguaje de propósito general, reusable para revisar maletas, documentos, o cualquier otra cosa que alguna vez necesite evaluar; la segunda (YAML, Kyverno) le da una lista que cualquier otro portero —sin entrenamiento previo en lógica formal— puede leer, entender y mantener el mismo día que empieza a trabajar.


La misma idea, dos sintaxis completas lado a lado

Antes de instalar nada (eso es la lección 6), vale la pena ver, en paralelo, cómo cada motor expresaría "todo contenedor debe declarar resources.limits y resources.requests para cpu y memory":

   GATEKEEPER (Rego dentro de un CRD)              KYVERNO (YAML puro)

   ConstraintTemplate                               ClusterPolicy
   ├─ define un CRD nuevo                            └─ NO define ningún CRD nuevo —
   │  (K8sRequiredResources)                             usa su propio tipo, ya instalado
   ├─ contiene lógica Rego                            └─ contiene un patrón YAML
   │  (violation[...] { ... })                            (spec.validate.pattern)
   │                                                       que se compara estructuralmente
   Constraint                                              contra el objeto entrante
   └─ instancia del CRD                              (una sola pieza, no dos)
      con match + parameters
      (dos piezas separadas)

Y, en código real, uno junto al otro:

# Gatekeeper — la lógica vive en el ConstraintTemplate (lección 3/4)
violation[{"msg": msg}] {
  container := input.review.object.spec.containers[_]
  required := input.parameters.limits
  provided := {key | container.resources.limits[key]}
  missing := {key | key := required[_]; not provided[key]}
  count(missing) > 0
  msg := sprintf("container <%v> is missing required resource limits: %v", [container.name, missing])
}
# Kyverno — la regla completa vive en un solo objeto (lección 6 la instala)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: andes-cargo-require-resource-limits
spec:
  validationFailureAction: Enforce
  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: "?*" }

Tres diferencias estructurales, todas visibles en ese código:

  • Gatekeeper necesita dos objetos (ConstraintTemplate + Constraint); Kyverno necesita uno solo (ClusterPolicy). No hay ningún paso equivalente a "generar un CRD nuevo" en Kyverno — ClusterPolicy ya es un CRD que Kyverno instala una sola vez, para todas las políticas futuras, y cada ClusterPolicy nueva es simplemente una instancia más de ese mismo tipo, con su propia lógica adentro.
  • Rego describe una condición que hace verdadera una violación; el pattern de Kyverno describe la forma que el objeto debe tener. El operador "?*" en el YAML de Kyverno significa, literalmente, "cualquier valor no vacío" — si resources.limits.cpu no existe, o existe pero está vacío, el patrón no coincide y la regla falla. Es una lógica de coincidencia estructural (¿este objeto tiene esta forma?), no una lógica declarativa de condiciones arbitrarias (¿bajo qué combinación de hechos es esto verdadero?).
  • validationFailureAction: Enforce en Kyverno es, en espíritu, el mismo interruptor que enforcementAction: deny en un Constraint de Gatekeeper — los dos motores ofrecen un modo "solo auditar, no bloquear" (Audit en Kyverno, dryrun en Gatekeeper) para probar una política nueva sin arriesgar romper nada en producción antes de confiar en ella.

Qué puede hacer Rego que el pattern de Kyverno, por diseño, no hace tan directo

El pattern de Kyverno es extraordinariamente legible para el caso de esta guía —"¿tiene este campo, sí o no?"—, pero su fortaleza (comparación estructural simple) es también su límite: expresar una condición que depende de combinar varios campos con lógica arbitraria (por ejemplo, "el límite de memoria debe ser al menos el doble del request de memoria", una comparación entre dos valores, no solo la presencia de cada uno) empieza a necesitar el operador deny/validate con expresiones más avanzadas de Kyverno —JMESPath, o directamente CEL (Common Expression Language) en las políticas más recientes del proyecto— que, en la práctica, se acerca cada vez más en expresividad a lo que Rego ya hacía de fábrica desde el principio. Rego, por diseño, no tiene ese límite: fue construido, desde el inicio, como un lenguaje de propósito general para lógica de políticas arbitrariamente compleja, con el costo de una curva de aprendizaje más pronunciada.


Cuándo un equipo elige cada uno — sin declarar un ganador

Ninguna de las dos siguientes listas es una recomendación de esta guía. Son los criterios reales, documentados por ambos proyectos y por la práctica de la industria, que inclinan la decisión hacia un lado u otro:

Un equipo tiende a Gatekeeper cuando:

  • Ya usa OPA/Rego en otro lugar del stack (por ejemplo, conftest sobre terraform plan, como cloud-security-and-guardrails-guide) y quiere reusar el mismo conocimiento de lenguaje en ambos contextos.
  • Necesita políticas con lógica combinatoria compleja —cruzar varios campos, iterar sobre estructuras anidadas con condiciones dependientes entre sí— donde la expresividad completa de un lenguaje de propósito general vale el costo de aprenderlo.
  • Quiere un motor de políticas potencialmente reusable más allá de Kubernetes —el mismo OPA que evalúa un Pod puede, en otros contextos fuera de esta guía, evaluar una respuesta de API o un archivo de configuración de un sistema distinto, sin cambiar de motor.

Un equipo tiende a Kyverno cuando:

  • La mayoría de su equipo ya sabe leer y escribir YAML de Kubernetes con fluidez, pero nadie en el equipo conoce Rego — el costo de entrenamiento de una política nueva es, literalmente, cero para alguien que ya escribe manifiestos de Kubernetes todos los días.
  • Las políticas necesarias son mayormente de forma "¿existe este campo?", "¿coincide este valor con este patrón?" — el caso más común en la práctica (exigir etiquetas, exigir límites de recursos, prohibir el tag latest, exigir readOnlyRootFilesystem: true), todas expresables sin lógica combinatoria compleja.
  • Necesita, además de validar, mutar objetos entrantes (inyectar una etiqueta faltante, agregar un valor por defecto) o generar recursos derivados automáticamente (por ejemplo, una NetworkPolicy default para cada namespace nuevo) — Kyverno construyó estas tres capacidades (validate, mutate, generate) bajo la misma sintaxis YAML consistente, algo que Gatekeeper también soporta pero con una curva de configuración distinta para cada capacidad.

La lección 6 instala Kyverno de verdad y corre exactamente la misma prueba de la lección 4 — el objetivo no es demostrar que uno "gana", es que veas, con evidencia real de los dos motores, que el resultado final para el usuario es idéntico (un Pod sin límites, rechazado; un Pod con límites, admitido), aunque el camino interno para llegar ahí sea distinto.


Errores comunes

Asumir que Kyverno es "Gatekeeper, pero más simple" en todos los casos (de simplificación excesiva). Qué pasa: alguien, después de ver que el YAML de Kyverno es más corto que el Rego de Gatekeeper para este ejemplo puntual, concluye que Kyverno siempre va a ser la opción más simple. Cómo detectarlo: si tu conclusión de esta lección es "Kyverno gana, es más fácil". Cómo corregirlo: para políticas de coincidencia estructural simple (el caso de este módulo), Kyverno efectivamente pide menos código. Para políticas que necesitan combinar condiciones de forma compleja —la sección anterior lo nombra explícitamente—, la sintaxis de Kyverno empieza a acercarse en complejidad a Rego, o directamente necesita expresiones adicionales (JMESPath, CEL) que no son más simples que Rego, solo distintas.

Pensar que hay que elegir uno de los dos para siempre, en todo el clúster (de decisión binaria innecesaria). Qué pasa: alguien asume que un clúster real solo puede tener un motor de políticas instalado a la vez. Cómo detectarlo: si tu plan es "voy a decidir cuál instalar y desinstalar el otro". Cómo corregirlo: técnicamente, los dos pueden coexistir en el mismo clúster —la lección 8 de este módulo, de hecho, lo hace a propósito, con los dos motores activos a la vez sobre andes-cargo, para documentar honestamente qué pasa cuando dos guardrails evalúan el mismo objeto—. En la práctica de un equipo real, sin embargo, mantener los dos motores activos con políticas superpuestas agrega complejidad operativa real (dos lugares donde buscar por qué algo fue rechazado); la mayoría de los equipos sí termina estandarizando en uno solo para reducir esa carga, aunque no exista ninguna limitación técnica que lo obligue.

Confundir validationFailureAction: Enforce con "esta política es la única que corre" (de alcance de un solo campo). Qué pasa: alguien lee Enforce y asume que es un ajuste global de Kyverno, no de esa ClusterPolicy específica. Cómo detectarlo: si esperas que cambiar Enforce a Audit en una política afecte el comportamiento de otra ClusterPolicy distinta. Cómo corregirlo: validationFailureAction es un campo de spec, específico de cada ClusterPolicy — cada política que instales, presente o futura, declara su propio modo, de forma completamente independiente de las demás.


Ejercicios

Ejercicio 1 — Traduce, en prosa, qué exige el pattern de esta lección, sin mirar el YAML de nuevo. Sin volver a mirar el bloque YAML de esta lección, explica en una frase qué condición exacta tiene que cumplir un Pod para pasar la política de Kyverno mostrada aquí.

Ver solución

Todo contenedor dentro del Pod tiene que declarar, simultáneamente, cuatro campos con algún valor no vacío: resources.requests.cpu, resources.requests.memory, resources.limits.cpu y resources.limits.memory. Si a cualquiera de esos cuatro campos le falta un valor (o el campo no existe en absoluto), el patrón no coincide con el objeto entrante, y la política dispara su rechazo.

Ejercicio 2 — Explica, sin usar la palabra "mejor", cuándo un equipo con conocimiento previo de Terraform/conftest elegiría Gatekeeper sobre Kyverno. Un compañero que ya trabajó cloud-security-and-guardrails-guide a fondo pregunta cuál de los dos motores debería aprender primero para este módulo. Sin usar la palabra "mejor" ni "peor", ¿qué argumento le darías a favor de empezar por Gatekeeper?

Ver solución

Una respuesta razonable: "Si ya escribiste reglas Rego para conftest, gran parte de ese conocimiento —el lenguaje declarativo, la forma de navegar input, el concepto de un conjunto violation/deny— es directamente reusable en Gatekeeper, con la diferencia de sintaxis que ya viste (input.review.object en vez de input a secas). Kyverno, en cambio, te pide aprender una convención nueva completa —la sintaxis de pattern, sus operadores especiales como ?*—, aunque esa convención sea, en sí misma, más simple de leer para alguien sin ningún conocimiento previo de Rego." La clave de una buena respuesta es que reconoce una ventaja de reuso de conocimiento existente, no una superioridad técnica objetiva de un motor sobre otro.

Ejercicio 3 — Predice qué pasaría si intentaras exigir "el límite de memoria debe ser el doble del request" con el pattern de Kyverno mostrado en esta lección. Sin investigar todavía la sintaxis avanzada de Kyverno, predice: ¿el pattern que viste en esta lección (con "?*") puede expresar directamente esa regla?

Ver solución

No, no directamente. El operador "?*" solo verifica presencia de un valor no vacío — no tiene ninguna forma de comparar el valor de un campo contra el valor de otro campo del mismo objeto (limits.memory contra requests.memory con una relación matemática entre los dos). Para expresar esa regla, Kyverno necesitaría una sintaxis de validación más avanzada que el pattern simple —fuera del alcance de este módulo—, mientras que Rego, por ser un lenguaje de propósito general, puede expresar esa comparación con un par de líneas adicionales dentro del mismo estilo de regla que ya viste en la lección 3. Este es, exactamente, el límite que la sección "Qué puede hacer Rego" de esta lección describió.


Resumen y siguiente paso

Esta lección puso, lado a lado, la misma política de límites de recursos escrita en Rego dentro de dos objetos (Gatekeeper) y en YAML dentro de uno solo (Kyverno) — dos idiomas distintos para la misma regla del portero. Viste las tres diferencias estructurales reales entre ambos (número de objetos, tipo de lógica, capacidades adicionales de Kyverno como mutar y generar), y los criterios honestos —sin ganador declarado— que inclinan a un equipo real hacia uno u otro.

Antes de avanzar deberías poder: escribir de memoria la diferencia estructural entre un ConstraintTemplate+Constraint y una ClusterPolicy; explicar qué significa el operador "?*" en un pattern de Kyverno; y nombrar, sin usar "mejor"/"peor", un criterio real que inclinaría a un equipo hacia cada motor.

Siguiente lección: manos a la obra — la misma política, en Kyverno. Ahí vas a instalar Kyverno v1.18.2 de verdad, aplicar la ClusterPolicy completa de esta lección, y correr la misma prueba de la lección 4 —Pod que viola, Pod que cumple— con Kyverno como el motor que decide.

Recursos

  1. Kyverno — Introduction — documentación oficial, con la comparación conceptual del proyecto entre YAML declarativo y otros motores de políticas.
  2. Kyverno — Validate Rules — referencia completa del campo pattern y sus operadores, incluido "?*".
  3. Open Policy Agent Gatekeeper — el motor de la lección 4, contrastado aquí punto por punto.
  4. Kyverno — Policy Settings — referencia oficial de validationFailureAction (Enforce/Audit), el equivalente conceptual de enforcementAction en Gatekeeper.