Admission control de Kubernetes para images de container: qué revisar en la puerta.
Un validating admission webhook es el único punto por el que pasa cada pod antes de ejecutarse, lo aplique quien lo aplique. Esta guía trata de qué debería exigir esa puerta a una image de container, qué hacer cuando aún no tiene respuesta y cómo comprobar que de verdad sigue en pie.
Por qué la puerta y no el postmortem
La seguridad runtime le avisa de que un container empezó a comportarse de forma extraña a las tres de la madrugada. El admission control decide si llega a arrancar. Responden a preguntas distintas, y acertar con la segunda es más barato: en la admission, el API server ya está esperando un sí o un no, la referencia de la image está en el pod spec y un rechazo cuesta un despliegue fallido, no un incidente.
Como el control está en la cadena de admission del API server, da igual quién despliegue: kubectl, Argo CD, Flux, un Job o un operator que reconcilia su propia CRD. Todo CREATE y UPDATE de pod fuera de una lista corta de namespaces exentos pasa por el mismo webhook, y una sincronización de GitOps que incumple la policy falla con el mensaje de rechazo como error, que es justo donde quiere leerlo.
Los cinco controles
Estas son las preguntas que una puerta de images puede responder a partir del pod spec y de una caché de escaneos, en el orden en que merece la pena activarlas. Las tres primeras no necesitan escáner.
- Higiene de tags. Un tag mutable puede cambiar lo que se ejecuta sin que nadie despliegue nada. Rechazar
:latestes la regla más pequeña posible y la que más merece tener desde el primer día. - Lista de registries permitidos. Todo lo que no venga de sus registries se rechaza sin más. Una lista vacía permite cualquier registry; una no vacía rechaza todos los demás.
- Fijación por digest. Tres niveles: permitir tags, preferir un digest, exigirlo. Exigir un digest es la garantía más fuerte sobre lo que se ejecutó y el despliegue más estricto, así que la mayoría de los entornos empiezan por «preferir» y endurecen namespace a namespace.
- Umbrales de vulnerabilidades. Rechazar ante cualquier hallazgo critical, ante cualquier high o por encima de un tope de hallazgos medium y low. Es el control que necesita un escáner detrás, y aquí es donde importa la sección siguiente.
- Firmas y attestations. Exigir una firma cosign verificada, una attestation SBOM y una attestation de escaneo de vulnerabilidades, y rechazar attestations que superen una antigüedad máxima. Actívelas cuando su pipeline de build las genere de verdad; antes, lo rechazan todo.
Además de la image en sí, la misma revisión puede examinar el workload: containers privilegiados, montajes hostPath y similares son hallazgos de bastionado, y una policy decide si rechazan, avisan o pasan.
Una image que nadie ha escaneado aún
Escanear lleva minutos; el API server espera segundos. Por eso una puerta que escanea en línea acaba agotando el tiempo de espera y llevándose los despliegues por delante. El diseño viable separa las dos cosas: un escáner llena una caché en segundo plano y la admission lee la caché. Así, un registry lento no puede ralentizar un despliegue.
La consecuencia que debe decidir antes de instalar: un digest que nadie ha visto antes aún no tiene resultado de escaneo. Hay dos respuestas honestas, y en Attestkeep el campo de policy admissionTiming elige una:
- Permitir y escanear. Admitir la image desconocida, poner el escaneo en cola de inmediato y decidir según el informe terminado en cada admission posterior de ese digest. Facilita la adopción y es el valor por defecto: nada de lo que despliegue el primer día se rechaza por ser nuevo.
- Rechazar hasta escanear. Rechazar la image desconocida con «a scan has been queued — retry shortly» y admitirla en cuanto exista el informe. Cada despliegue de un digest nuevo cuesta un reintento. Elíjalo para los namespaces que se lo hayan ganado.
En los dos casos, el primer escaneo de un digest nuevo se ejecuta en segundo plano. En un cluster recién creado puede tardar unos minutos (el escáner descarga la image y la base de vulnerabilidades está fría), y hasta que termina la image es unknown, nunca compliant. Los dos ajustes están en todas las ediciones de Attestkeep, Community incluida.
Fail-open o fail-closed
La otra decisión de postura no tiene que ver con la image, sino con la propia puerta. ¿Qué debe hacer el API server cuando su webhook no responde? Kubernetes lo llama la failurePolicy del webhook, y ninguno de los dos valores es correcto en todos los casos.
- Ignore (fail-open) mantiene el cluster desplegable durante una caída del webhook y deja un hueco: durante esa ventana no se revisó nada. Un control fiable registra esa ventana en lugar de mostrar en silencio un trimestre limpio.
- Fail (fail-closed) le da un control que, de forma demostrable, nunca tuvo un hueco, y significa que una caída del webhook detiene los despliegues. Es un coste real, que hay que asumir a propósito y respaldar con el número de réplicas y la monitorización.
Lo que pasó durante un hueco sigue ejecutándose cuando el hueco se cierra. Lo encuentra una conciliación periódica: comparar el digest de image observado en cada container en marcha con el ledger de admission e informar de los que no tienen una decisión detrás (o detenerlos y volver a admitirlos). Attestkeep viene con Ignore por defecto, registra cada ventana de caída para el documento de evidencia y ejecuta esa conciliación cada hora.
Pruebe que sigue en pie
Un admission webhook que en silencio no está funcionando parece exactamente un cluster conforme. Por eso, lo primero que hay que hacer tras instalar cualquier puerta, y tras cada actualización, es verla rechazar algo. Con la policy por defecto de Attestkeep, la infracción más pequeña es el tag :latest:
kubectl run enforcement-check --image=nginx:latest
Error from server: admission webhook "image-security.attestkeep.com" denied the request:
image security policy: nginx:latest: the :latest tag is blocked by policy
kubectl get pod enforcement-check
Error from server (NotFound): pods "enforcement-check" not foundHay dos cosas que conviene notar. El error viene de Error from server: el rechazo ocurrió dentro de la cadena de admission del API server, no en un agente que podría estar dormido. Y el motivo nombra la regla, así que el desarrollador que lo lea en un log de CI sabe qué corregir. Si en cambio se creó el pod, revise los sospechosos habituales por orden: un namespace exento, una policy en modo audit, un webhook en fail-open, una instalación que nunca se activó.
Guarde el registro
Un rechazo que no deja registro es enforcement sin evidencia. Registre lo admitido además de lo rechazado (una evidencia que solo enumera rechazos no puede demostrar que un control funcionaba) y registre cada decisión bajo el hash de la policy que la tomó, para que «qué regla estaba en vigor cuando se admitió esto» sea una consulta y no arqueología en el historial de git.
Esa es la parte que no le dan las herramientas gratuitas y la que de verdad lee un auditor. Attestkeep calcula el hash del contenido de cada decisión al escribirla, sella el ledger bajo checkpoints firmados y encadenados, y convierte las decisiones de un periodo en un paquete de evidencia firmado, vinculado al conjunto de controles que elija. Cómo se consigue que ese ledger delate cualquier manipulación, y dónde terminan exactamente sus garantías, se explica en la página evidence trust model (en inglés).
Cada ajuste mencionado aquí está documentado: Policies para la referencia de campos y ejemplos listos para aplicar, Admission para los dos ajustes de postura y Prove it is enforcing para el runbook completo. La edición Community es gratuita e incluye todos los controles de esta página.