Módulo 6: Runtime Security Admission Control And Image Scanning
3. OPA Gatekeeper: `ConstraintTemplate` y `Constraint`
Descripción
La lección 2 dejó claro dónde corre un admission controller (la fase 3 del ciclo de vida de un request, antes de etcd). Esta lección resuelve cómo OPA Gatekeeper —el primero de los dos motores de este módulo— convierte una regla Rego, el mismo lenguaje declarativo que cloud-security-and-guardrails-guide ya usó con conftest, en un objeto que Kubernetes entiende, valida con su propio esquema, y aplica en vivo contra cada Pod que intenta nacer. La lección 4 instala Gatekeeper y ejecuta esto de verdad; esta lección, primero, separa el concepto de la sintaxis, exactamente como la lección 2 del Módulo 4 de cloud-security-and-guardrails-guide hizo antes de instalar conftest.
Conexión con el módulo
Gatekeeper no reinventa Rego — lo empaqueta. La pieza nueva que esta lección enseña no es el lenguaje (ya lo conoces si trabajaste cloud-security-and-guardrails-guide), es el contenedor: dos Custom Resource Definitions (CRD) de Kubernetes, ConstraintTemplate y Constraint, que convierten un archivo .rego —algo que conftest lee del disco— en un objeto de Kubernetes que vive dentro del clúster, con su propio apiVersion, su propio kind, y su propio ciclo de vida gestionado por kubectl.
conftest frente a Gatekeeper: el mismo lenguaje, dos arquitecturas completamente distintas
Antes de entrar en la sintaxis de Gatekeeper, vale la pena dejar esta comparación fija, porque es el error conceptual más común al llegar desde cloud-security-and-guardrails-guide:
conftest (cloud-security-and-guardrails-guide, M4) Gatekeeper (este módulo)
┌──────────────────┐ ┌──────────────────────┐
│ terraform plan │ │ kubectl apply -f │
│ (comando manual) │ │ pod.yaml (o ArgoCD) │
└─────────┬──────────┘ └──────────┬────────────┘
│ genera │ HTTP request
▼ ▼
┌──────────────────┐ FUERA DEL CLÚSTER ┌──────────────────────┐
│ plan.json │ (archivo en disco) │ kube-apiserver │ DENTRO DEL CLÚSTER
└─────────┬──────────┘ └──────────┬────────────┘ (webhook HTTP)
│ conftest test plan.json │ reenvía el objeto
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ motor OPA │ │ gatekeeper- │
│ (proceso CLI, │ │ controller-manager │
│ corre y termina) │ │ (Pod que vive │
└─────────┬──────────┘ │ permanentemente) │
│ PASS / FAIL └──────────┬────────────┘
▼ │ allowed: true/false
tu terminal, tu CI ▼
(nada se creó todavía el objeto EXISTE o NUNCA
en la infraestructura real) llegó a existir
La diferencia no es de rigor ni de calidad —ambos son motores OPA reales, evaluando Rego real—. Es de arquitectura y momento:
conftestes un binario que corre y termina. Lo invocas desde tu terminal o desde un job de CI, le pasas un archivo (plan.json), evalúa, imprimePASS/FAIL, y el proceso muere. No hay ningún componente deconftestcorriendo permanentemente en ningún lado — no existe hasta que lo ejecutas, y deja de existir en cuanto termina.- Gatekeeper es un
Deploymentque vive dentro del clúster, indefinidamente. No lo "invocas" — está siempre ahí, corriendo tres réplicas (lo vas a ver en la lección 4), esperando quekube-apiserverle reenvíe cada objeto nuevo vía elValidatingWebhookConfigurationde la lección 2. Nunca lo llamas directamente;kube-apiserverlo hace por ti, en cadakubectl apply, sin que nadie tenga que acordarse de correr ningún comando. - El
inputque cada uno evalúa es de naturaleza distinta. Elinputdeconftestes un archivoplan.json— una fotografía de lo que Terraform planea hacer, generada por un comando explícito, en un momento controlado. Elinputde Gatekeeper es el objeto JSON completo delAdmissionReviewquekube-apiserverle envía en tiempo real — no hay ningún paso intermedio de "generar un plan": el objeto que estás intentando crear es elinput, en el instante exacto del intento.
Esta es la razón técnica exacta por la que la cita del Módulo 1 de esta guía (lección 1) distingue "un plan de Terraform fuera del clúster" de "objetos en vivo, dentro del clúster" — no es una frase decorativa, es la diferencia de arquitectura completa entre las dos herramientas.
ConstraintTemplate: la regla, sin decir todavía a qué se aplica
Un ConstraintTemplate es un objeto de Kubernetes —apiVersion: templates.gatekeeper.sh/v1— que hace dos cosas a la vez: define la lógica Rego de una regla, y define la forma (el esquema) de los parámetros que esa regla va a aceptar. Es, en espíritu, como declarar una función reusable: escribes la lógica una sola vez, y después la aplicas con distintos parámetros tantas veces como quieras.
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredresources
spec:
crd:
spec:
names:
kind: K8sRequiredResources
validation:
openAPIV3Schema:
type: object
properties:
limits:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredresources
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])
}
Cuatro piezas, con un trabajo concreto cada una:
spec.crd.spec.names.kind: K8sRequiredResources— este es el nombre que Kubernetes va a usar para el nuevo tipo de recurso que Gatekeeper crea automáticamente a partir de esteConstraintTemplate. Después de aplicar este YAML,kubectl get k8srequiredresourcesva a funcionar como comando válido, exactamente comokubectl get podsokubectl get deployments— Gatekeeper registra un CRD nuevo, en vivo, sin que tú tengas que escribir el CRD a mano.spec.crd.spec.validation.openAPIV3Schema— declara qué parámetros va a aceptar cadaConstraintque use esta plantilla (en este caso, un campolimits, un arreglo de strings). Esto es lo que hace que la misma lógica Rego sea reusable: la próxima vez que necesites exigir un recurso distinto —por ejemplo,ephemeral-storage— no reescribes la regla, solo cambias el parámetro.spec.targets[].target: admission.k8s.gatekeeper.sh— le dice a Gatekeeper que esta regla evalúa objetos de Kubernetes en el momento de admission (a diferencia de otrostargetposibles, fuera del alcance de este módulo, que Gatekeeper soporta para auditar recursos ya existentes).spec.targets[].rego— la lógica misma. Fíjate eninput.review.object— no esinputa secas, como enconftest: Gatekeeper envuelve el objeto completo dentro de una estructuraAdmissionReview, y tu regla navegainput.review.objectpara llegar alPodreal. Esta es, en la práctica, la diferencia de sintaxis más visible entre una regla escrita paraconftesty una escrita para Gatekeeper — la lógica declarativa (violation[...] { condiciones }) es la misma idea quedeny contains msg if { ... }decloud-security-and-guardrails-guide, con una diferencia real de sintaxis Rego: Gatekeeper, en sus ejemplos oficiales, usa todavía la formaviolation[{"msg": msg}] { ... }(Rego "clásico", sin la palabraifnicontainsexplícitas), mientras que las versiones recientes de OPA/conftestpromueven la forma más nuevadeny contains msg if { ... }. Las dos formas son válidas Rego —el motor subyacente las acepta indistintamente—; la diferencia es de estilo entre generaciones de ejemplos, no de capacidad.
Constraint: aplicar la plantilla, con parámetros concretos
Una vez que el ConstraintTemplate existe —y Kubernetes ya reconoce K8sRequiredResources como un tipo válido—, un Constraint es la instancia concreta: a qué objetos se aplica, y con qué parámetros.
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredResources
metadata:
name: andes-cargo-must-have-resource-limits
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- "andes-cargo"
parameters:
limits:
- cpu
- memory
Fíjate en el apiVersion y el kind: apiVersion: constraints.gatekeeper.sh/v1beta1, kind: K8sRequiredResources — este último es, exactamente, el nombre que el ConstraintTemplate anterior declaró en spec.crd.spec.names.kind. No es coincidencia: Gatekeeper generó ese CRD automáticamente al aplicar el ConstraintTemplate, y este Constraint es una instancia de ese CRD nuevo, tal como un Pod es una instancia del tipo Pod que ya existía de fábrica en Kubernetes.
spec.match— a qué objetos aplica esta instancia específica:Pod, solo en el namespaceandes-cargo. Este campo es lo que te permite tener la misma lógica Rego (un soloConstraintTemplate) aplicada con alcances distintos en distintosConstraint— por ejemplo, una versión más estricta paraandes-cargoy otra más laxa parakube-system, sin duplicar ni una línea de Rego.spec.parameters— los valores concretos para el esquema que elConstraintTemplatedefinió: en este caso,limits: [cpu, memory], exigiendo que todo contenedor declare ambos.
La relación entre los dos objetos, resumida en una frase: el ConstraintTemplate es la clase; el Constraint es la instancia. Puedes tener un ConstraintTemplate y diez Constraint distintos usándolo, cada uno con su propio match y sus propios parameters — y si mañana necesitas corregir un bug en la lógica Rego, lo corriges una sola vez, en el ConstraintTemplate, y los diez Constraint heredan la corrección automáticamente.
Errores comunes
Intentar aplicar un Constraint antes de que su ConstraintTemplate termine de registrarse (de orden de operaciones). Qué pasa: alguien aplica los dos archivos con kubectl apply -f . en la misma orden alfabética en la que aparecen en una carpeta, y el Constraint falla con un error como no matches for kind "K8sRequiredResources". Cómo detectarlo: si el mensaje de error menciona explícitamente que Kubernetes no reconoce el kind que estás aplicando. Cómo corregirlo: Gatekeeper necesita unos segundos para leer el ConstraintTemplate, generar el CRD nuevo, y registrarlo contra kube-apiserver — es un proceso asíncrono, no instantáneo. La lección 4 aplica los dos por separado, con una pausa explícita entre uno y otro, exactamente por esta razón.
Escribir Rego para Gatekeeper usando input.spec en vez de input.review.object.spec (de sintaxis heredada de conftest). Qué pasa: alguien que ya escribió reglas para conftest en cloud-security-and-guardrails-guide intenta reusar la misma navegación de input (input.environment, input.debug, del ejemplo de esa guía) directamente en una regla de Gatekeeper. Cómo detectarlo: si tu regla nunca dispara, ni siquiera contra un objeto que claramente debería violarla. Cómo corregirlo: el input que conftest recibe es el archivo que le pasaste tal cual; el input que Gatekeeper recibe es un AdmissionReview completo, con el objeto real anidado dentro de input.review.object. Es la misma variable especial input, poblada por dos sistemas distintos, con dos formas distintas.
Confundir el apiVersion/kind del ConstraintTemplate con el del Constraint (de los dos objetos con nombres parecidos). Qué pasa: alguien copia apiVersion: templates.gatekeeper.sh/v1 al escribir el Constraint, o viceversa. Cómo detectarlo: kubectl apply responde con un error de validación de esquema, o el objeto se crea pero nunca aparece en kubectl get k8srequiredresources. Cómo corregirlo: son dos apiVersion distintos, a propósito — templates.gatekeeper.sh/v1 es fijo, el mismo para cualquier ConstraintTemplate que escribas; constraints.gatekeeper.sh/v1beta1 (o el grupo de API que corresponda al kind específico) es el que usa cada instancia de Constraint, y su kind cambia según qué ConstraintTemplate estés instanciando.
Ejercicios
Ejercicio 1 — Identifica cuál de los dos objetos cambiarías para agregar ephemeral-storage a la lista de recursos exigidos. Sin volver a mirar el YAML de esta lección, decide: para exigir que los Pods de andes-cargo también declaren límites de ephemeral-storage (además de cpu y memory), ¿modificarías el ConstraintTemplate, el Constraint, o los dos?
Ver solución
Solo el Constraint — específicamente, el campo spec.parameters.limits, agregando "ephemeral-storage" al arreglo ["cpu", "memory"]. El ConstraintTemplate ya declaró que limits es un arreglo de strings genérico, sin fijar de antemano cuáles strings son válidos — la lógica Rego itera sobre lo que sea que input.parameters.limits contenga, sin importar cuántos elementos tenga ni cuáles sean. Si tu respuesta fue "los dos", repasa la sección "Constraint: aplicar la plantilla" — la separación entre lógica reusable (plantilla) y parámetros concretos (instancia) es exactamente lo que evita tener que tocar Rego para este tipo de cambio.
Ejercicio 2 — Explica, con tus propias palabras, por qué Gatekeeper no puede evaluar un terraform plan. Un compañero pregunta: "si Gatekeeper también usa Rego, ¿podría usarlo para reemplazar conftest en cloud-security-and-guardrails-guide, y evaluar el plan de Terraform ahí mismo?". ¿Qué le responderías?
Ver solución
Técnicamente, no sin trabajo adicional considerable — y no porque Rego no pueda expresar la lógica (podría), sino porque la arquitectura de Gatekeeper depende de estar conectado a un ValidatingWebhookConfiguration de un clúster de Kubernetes real, recibiendo AdmissionReview de objetos que kube-apiserver intenta crear. Un terraform plan no pasa nunca por kube-apiserver — no hay ningún momento en el ciclo de vida de un apply de Terraform en el que exista un AdmissionReview que evaluar. conftest, en cambio, fue diseñado exactamente para el caso contrario: evaluar cualquier archivo estructurado, sin necesitar ningún clúster ni ningún webhook detrás. Los dos comparten el motor (OPA) y el lenguaje (Rego), pero están construidos para arquitecturas de entrada completamente distintas — uno espera un archivo en disco, el otro espera un webhook HTTP en vivo.
Ejercicio 3 — Predice qué pasa si borras un ConstraintTemplate mientras un Constraint que lo usa sigue existiendo. Sin ejecutar nada todavía (eso lo vas a confirmar en la lección 4), predice: si corres kubectl delete constrainttemplate k8srequiredresources mientras andes-cargo-must-have-resource-limits (el Constraint) sigue existiendo, ¿qué esperarías que pase con el Constraint, y con la protección que ofrecía?
Ver solución
Kubernetes elimina el CRD K8sRequiredResources completo junto con el ConstraintTemplate que lo generó —y, por la semántica estándar de Kubernetes ante la eliminación de un CRD, cualquier instancia de ese tipo (incluido tu Constraint) se elimina en cascada también—. El resultado práctico: la protección desaparece por completo, de inmediato, sin ningún aviso adicional más allá de lo que kubectl te muestre en el momento del delete. Es la misma relación de dependencia fuerte que existe entre cualquier CRD y sus recursos personalizados en Kubernetes — borrar la definición del tipo se lleva todas sus instancias con él.
Resumen y siguiente paso
Esta lección separó, con precisión, el concepto de Gatekeeper de la sintaxis que la lección 4 va a ejecutar de verdad: un ConstraintTemplate (la lógica Rego reusable, más el esquema de sus parámetros) y un Constraint (la instancia concreta, con match y parameters específicos) — la diferencia entre una clase y un objeto, aplicada a políticas. Viste, con el diagrama completo, por qué Gatekeeper y conftest comparten lenguaje (Rego) pero no arquitectura: uno evalúa un archivo estático generado antes de cualquier apply, fuera del clúster; el otro evalúa un objeto en vivo, dentro del clúster, en el instante exacto de cada intento de creación.
Antes de avanzar deberías poder: explicar la relación entre un ConstraintTemplate y un Constraint sin ayuda; distinguir input.parameters de input.environment (la sintaxis de conftest) de input.review.object (la sintaxis de Gatekeeper); y explicar, con tus propias palabras, por qué Gatekeeper no puede evaluar un terraform plan.
Siguiente lección: manos a la obra — instalando Gatekeeper y tu primera política. Ahí vas a aplicar el manifiesto oficial v3.23.0 contra andes-cargo-cluster, aplicar el ConstraintTemplate y el Constraint de esta lección de verdad, y ver el mensaje literal que Gatekeeper devuelve cuando un Pod real intenta violarlo.
Recursos
cloud-security-and-guardrails-guide(NIEVA), Módulo 4, lección 2 — la base de Rego (deny contains msg if { ... }) que esta lección contrasta con la sintaxis deConstraintTemplatede Gatekeeper.- Gatekeeper — How to use Gatekeeper — guía oficial de
ConstraintTemplateyConstraint, con ejemplos completos. - Gatekeeper — Constraint Templates — referencia detallada del CRD que esta lección diseccionó.
- Open Policy Agent — Policy Language (Rego) — referencia completa del lenguaje, la misma que
cloud-security-and-guardrails-guidecitó paraconftest.