Kyverno es un motor de políticas para Kubernetes que funciona como controlador de admisión. A diferencia de OPA Gatekeeper, las políticas se escriben en YAML con la misma estructura que los recursos que validan, sin aprender Rego. En este tutorial instalarás Kyverno con Helm y crearás políticas de los cuatro tipos principales: validación (rechazar lo que no cumple), mutación (completar recursos automáticamente), generación (crear recursos asociados) y verificación de firmas de imágenes.

Requisitos previos

Para seguir esta guía necesitas:

  • Un clúster Kubernetes en una versión mantenida (1.30 o superior), por ejemplo un clúster Kubernetes gestionado de CubePath o uno propio con kubeadm o k3s.
  • kubectl configurado con permisos de cluster-admin.
  • Helm 3 instalado en tu equipo.
  • Para la política de generación, un CNI que aplique NetworkPolicies (Calico o Cilium, por ejemplo).
  • Para la verificación de imágenes, una imagen firmada con Cosign y su clave pública (consulta la guía de firma de imágenes con Cosign).

Paso 1: Instalar Kyverno con Helm

Añade el repositorio oficial y actualiza el índice:

helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update

Instala Kyverno en su propio namespace. Para un entorno de pruebas basta con los valores por defecto:

helm install kyverno kyverno/kyverno \
  --namespace kyverno \
  --create-namespace

En producción, ejecuta varias réplicas de cada controlador para que el webhook siga disponible durante actualizaciones o fallos de un nodo:

helm upgrade --install kyverno kyverno/kyverno \
  --namespace kyverno \
  --create-namespace \
  --set admissionController.replicas=3 \
  --set backgroundController.replicas=2 \
  --set cleanupController.replicas=2 \
  --set reportsController.replicas=2

Comprueba que los controladores están en marcha:

kubectl get pods -n kyverno
NAME                                             READY   STATUS    RESTARTS   AGE
kyverno-admission-controller-6d8f9c7b5d-x2k8p    1/1     Running   0          70s
kyverno-background-controller-5b7c9d8f6c-m4q9z   1/1     Running   0          70s
kyverno-cleanup-controller-7f6d8b9c5d-p7w2n      1/1     Running   0          70s
kyverno-reports-controller-8c5d7f9b6d-h3t6r      1/1     Running   0          70s

Cada controlador tiene una función: el de admisión valida y muta las peticiones, el de background ejecuta las reglas de generación y mutación de recursos existentes, el de limpieza borra recursos según políticas de limpieza y el de informes genera los PolicyReport.

Crea un directorio para los manifiestos y un namespace de pruebas:

mkdir -p ~/kyverno && cd ~/kyverno
kubectl create namespace demo

Paso 2: Crear una política de validación en modo Audit

Las políticas se definen con ClusterPolicy (todo el clúster) o Policy (un solo namespace). Cada regla indica a qué recursos se aplica (match), qué excluye (exclude) y qué hace. Empieza exigiendo la etiqueta app.kubernetes.io/name en los Pods, en modo Audit para no bloquear nada todavía:

nano require-labels.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
spec:
  background: true
  rules:
    - name: check-app-name-label
      match:
        any:
          - resources:
              kinds: ["Pod"]
      exclude:
        any:
          - resources:
              namespaces: ["kube-system", "kyverno"]
      validate:
        failureAction: Audit
        message: "La etiqueta app.kubernetes.io/name es obligatoria."
        pattern:
          metadata:
            labels:
              app.kubernetes.io/name: "?*"

El patrón "?*" significa "cualquier valor con al menos un carácter". background: true hace que la política también evalúe los recursos que ya existían.

kubectl apply -f require-labels.yaml
kubectl get clusterpolicy require-labels
NAME             ADMISSION   BACKGROUND   READY   AGE   MESSAGE
require-labels   true        true         True    8s    Ready

Aunque la regla se refiere a Pods, Kyverno genera automáticamente reglas equivalentes para Deployments, StatefulSets, DaemonSets, Jobs y CronJobs (autogen), de modo que el error se detecta en el propio Deployment y no solo en los Pods que crea. Puedes ver las reglas generadas con kubectl get clusterpolicy require-labels -o yaml, en status.autogen.

Paso 3: Revisar los informes de auditoría

Crea un Deployment que incumple la política:

kubectl create deployment web --image=nginx:1.27 -n demo

En modo Audit se acepta, pero queda registrado en un PolicyReport. Espera unos segundos y consulta los informes del namespace:

kubectl get policyreport -n demo
NAME                                   KIND         NAME                    PASS   FAIL   WARN   ERROR   SKIP   AGE
3b1c2d4e-5f6a-7b8c-9d0e-1f2a3b4c5d6e   Deployment   web                     0      1      0      0       0      15s
8a7b6c5d-4e3f-2a1b-0c9d-8e7f6a5b4c3d   Pod          web-6d4cf56db6-kq8x2    0      1      0      0       0      15s

Para ver el motivo del fallo:

kubectl get policyreport -n demo -o jsonpath='{range .items[*].results[*]}{.policy}/{.rule}: {.result} - {.message}{"\n"}{end}'

Para una visión global de todo el clúster, usa kubectl get policyreport -A. Corrige los recursos que fallan antes de pasar la política a Enforce.

Paso 4: Bloquear recursos en modo Enforce

Esta segunda política prohíbe usar el tag latest (o no indicar tag), porque impide saber qué versión se está ejecutando. Se aplica directamente en Enforce:

nano disallow-latest-tag.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
spec:
  background: true
  rules:
    - name: require-image-tag
      match:
        any:
          - resources:
              kinds: ["Pod"]
      exclude:
        any:
          - resources:
              namespaces: ["kube-system", "kyverno"]
      validate:
        failureAction: Enforce
        message: "Indica un tag de imagen concreto."
        pattern:
          spec:
            containers:
              - image: "*:*"
    - name: validate-image-tag
      match:
        any:
          - resources:
              kinds: ["Pod"]
      exclude:
        any:
          - resources:
              namespaces: ["kube-system", "kyverno"]
      validate:
        failureAction: Enforce
        message: "No se permite el tag latest."
        pattern:
          spec:
            containers:
              - image: "!*:latest"
kubectl apply -f disallow-latest-tag.yaml

Prueba a desplegar una imagen con latest:

kubectl create deployment prueba --image=nginx:latest -n demo
error: failed to create deployment: admission webhook "validate.kyverno.svc-fail" denied the request:

resource Deployment/demo/prueba was blocked due to the following policies

disallow-latest-tag:
  autogen-validate-image-tag: 'validation error: No se permite el tag latest. rule
    autogen-validate-image-tag failed at path /spec/template/spec/containers/0/image/'

El prefijo autogen- confirma que la regla generada para Deployments ha actuado. Con un tag concreto, como nginx:1.27, el Deployment se crea sin problemas.

Paso 5: Completar recursos con una política de mutación

Las políticas de mutación modifican el recurso antes de guardarlo. Esta añade la etiqueta team: sin-asignar a los Pods que no la tengan. El prefijo +( ) indica "añadir solo si no existe", así que nunca sobrescribe un valor que el usuario haya puesto:

nano add-team-label.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-team-label
spec:
  rules:
    - name: add-default-team
      match:
        any:
          - resources:
              kinds: ["Pod"]
      exclude:
        any:
          - resources:
              namespaces: ["kube-system", "kyverno"]
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              +(team): sin-asignar
kubectl apply -f add-team-label.yaml
kubectl run mutado --image=nginx:1.27 -n demo
kubectl get pod mutado -n demo --show-labels
NAME     READY   STATUS    RESTARTS   AGE   LABELS
mutado   1/1     Running   0          5s    run=mutado,team=sin-asignar

Paso 6: Generar recursos automáticamente

Las políticas de generación crean recursos cuando aparece otro. Un caso habitual es añadir una NetworkPolicy que deniega el tráfico entrante en cada namespace nuevo, para que los equipos abran explícitamente lo que necesitan.

Kyverno necesita permiso para crear NetworkPolicies. Concédeselo con un ClusterRole que el chart agrega automáticamente a sus controladores mediante etiquetas:

nano kyverno-networkpolicies-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kyverno:manage-networkpolicies
  labels:
    rbac.kyverno.io/aggregate-to-background-controller: "true"
    rbac.kyverno.io/aggregate-to-admission-controller: "true"
rules:
  - apiGroups: ["networking.k8s.io"]
    resources: ["networkpolicies"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

Crea la política:

nano add-default-deny.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-deny
spec:
  rules:
    - name: default-deny-ingress
      match:
        any:
          - resources:
              kinds: ["Namespace"]
      exclude:
        any:
          - resources:
              names: ["kube-*", "kyverno", "default"]
      generate:
        apiVersion: networking.k8s.io/v1
        kind: NetworkPolicy
        name: default-deny-ingress
        namespace: "{{request.object.metadata.name}}"
        synchronize: true
        data:
          spec:
            podSelector: {}
            policyTypes:
              - Ingress

synchronize: true hace que Kyverno vuelva a crear la NetworkPolicy si alguien la borra y la actualice si cambias la política. Aplica ambos archivos y crea un namespace para comprobarlo:

kubectl apply -f kyverno-networkpolicies-rbac.yaml
kubectl apply -f add-default-deny.yaml
kubectl create namespace equipo-a
kubectl get networkpolicy -n equipo-a
NAME                   POD-SELECTOR   AGE
default-deny-ingress   <none>         3s

Paso 7: Exigir imágenes firmadas con Cosign

Las reglas verifyImages comprueban la firma de cada imagen antes de admitir el Pod. Sustituye el bloque de clave pública por el contenido de tu cosign.pub y ghcr.io/your_user/* por el prefijo de tus imágenes:

nano verify-images.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  background: false
  rules:
    - name: check-cosign-signature
      match:
        any:
          - resources:
              kinds: ["Pod"]
      verifyImages:
        - imageReferences:
            - "ghcr.io/your_user/*"
          failureAction: Enforce
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      your_cosign_public_key
                      -----END PUBLIC KEY-----
kubectl apply -f verify-images.yaml

Prueba con una imagen firmada y otra sin firmar del mismo registro:

kubectl run firmada --image=ghcr.io/your_user/demo-app:v1.0.0 -n demo
kubectl run sin-firma --image=ghcr.io/your_user/otra-app:v1.0.0 -n demo

La primera se crea y la segunda se rechaza con un mensaje que incluye failed to verify image y no signatures found. Además, Kyverno reescribe la imagen admitida para fijarla por digest, así que el Pod ejecuta exactamente el contenido verificado:

kubectl get pod firmada -n demo -o jsonpath='{.spec.containers[0].image}{"\n"}'
ghcr.io/your_user/demo-app:v1.0.0@sha256:4f1c3e8a9b2d...

Paso 8: Aplicar las políticas de seguridad estándar

Kyverno publica un chart con las políticas de Pod Security Standards (niveles baseline y restricted), útil como punto de partida:

helm install kyverno-policies kyverno/kyverno-policies \
  --namespace kyverno \
  --set podSecurityStandard=baseline
kubectl get clusterpolicy

Estas políticas se instalan en modo Audit por defecto. Revisa los PolicyReport durante unos días y pasa a Enforce las que no generen falsos positivos.

Solución de problemas

La política no pasa a READY True. Revisa el mensaje de estado; suele ser un error de sintaxis o falta de permisos en una regla de generación:

kubectl describe clusterpolicy add-default-deny

Una regla de generación no crea nada. Revisa los logs del controlador de background:

kubectl logs -n kyverno deployment/kyverno-background-controller

Todas las peticiones se ralentizan o fallan con context deadline exceeded. El webhook de admisión no responde a tiempo. Comprueba que los pods de kyverno-admission-controller están sanos y tienen CPU suficiente; las reglas verifyImages contra registros lentos son la causa más habitual.

Necesitas desactivar temporalmente una política. Pásala a Audit o elimínala con kubectl delete clusterpolicy <nombre>. No borres los webhooks a mano: Kyverno los gestiona y los volverá a crear.

Conclusión

Has instalado Kyverno con Helm y has creado políticas de validación en Audit y Enforce, una mutación que completa etiquetas, una generación de NetworkPolicies por namespace y una verificación de firmas de Cosign. Como siguientes pasos, guarda las políticas en Git y despliégalas con Argo CD o Flux, prueba las políticas en CI con la CLI de Kyverno (kyverno apply y kyverno test) y explora la biblioteca de políticas de kyverno.io para casos como límites de recursos o registros permitidos.