Llega una CVE nueva con un CVSS por encima de 9, un número de advisory de runc o containerd y un resumen de una línea que dice "container breakout" o "escalada de privilegios". Parchear tus contenedores de aplicación es la parte fácil: reconstruyes la imagen, haces push y dejas que el deployment haga el rollout. Parchear los nodos sobre los que corren esos contenedores es un problema completamente distinto, y es el que hace correr a la mayoría de equipos.
El parcheo a nivel de aplicación es algo que tu pipeline de CI/CD ya hace en cada merge. El parcheo a nivel de nodo toca las instancias EC2 que respaldan tu cluster de EKS, la AMI desde la que arrancaron, el container runtime que corre por debajo de kubelet y, potencialmente, el propio kernel. Nada de eso se reconstruye con un cambio en el Dockerfile. Requiere sustituir máquinas, y sustituir máquinas manteniendo cada pod programado y cada PodDisruptionBudget satisfecho es donde está el trabajo de ingeniería real.
Triage: decidir con qué urgencia hay que moverse
No toda CVE justifica una ventana de cambio de emergencia a las 11 de la noche. Lo primero que hacemos cuando sale una nueva es determinar si es un "se parchea esta semana en el ciclo de mantenimiento normal" o un "se parchea esta noche". Tres preguntas guían esa decisión.
¿Es realmente explotable desde donde estás?
Un CVSS base score te dice lo grave que es la vulnerabilidad en abstracto, no lo expuesto que estás tú. Una CVE de container escape en runc que requiere que el atacante ya controle un contenedor con capabilities específicas es seria, pero tiene un perfil de riesgo distinto al de una CVE en kubelet explotable por red sin autenticación. Revisa el vector de ataque (AV:N de red frente a AV:L local) y si la explotación requiere privilegios que no concedes a los workloads de tus tenants. Si no corres workloads no confiables o multi-tenant en un cluster, una CVE que solo permite escalada de privilegios local tiene menos urgencia de la que parece sobre el papel, aunque igual hay que parchearla antes del próximo despliegue de código no confiable.
¿La CVE afecta siquiera a tu AMI?
Este es el paso que la gente se salta y no debería. No toda CVE de containerd o runc afecta a todas las AMI optimizadas para EKS, porque Bottlerocket, Amazon Linux 2023 y una AMI custom basada en Ubuntu o AL2 pueden estar corriendo versiones distintas de los mismos componentes, parcheadas en cadencias distintas. Bottlerocket viene con un sistema operativo mínimo e inmutable, con una superficie de ataque mucho más pequeña y su propia integración endurecida del container runtime, así que a veces no se ve afectado por una CVE que sí golpea a distribuciones de propósito general — pero eso hay que verificarlo, no asumirlo.
AWS publica boletines de seguridad específicos para las AMI optimizadas para EKS, y las release notes de las AMI de EKS en GitHub listan las versiones exactas de containerd, runc y kernel empaquetadas en cada release de AMI. Cruza el rango de versiones afectadas de la CVE contra lo que realmente lleva la AMI que corren tus node groups — aws eks describe-nodegroup o, en Bottlerocket, consultar el VERSION_ID en ejecución vía apiclient te da la versión actual rápido. Si tu AMI ya está en un release parcheado, no tienes que hacer nada más que confirmarlo, algo que pasa más a menudo de lo que la gente espera porque EKS publica AMIs parcheadas rápido tras la divulgación upstream.
¿Cuál es tu ventana de exposición real?
Si la CVE es explotable por red, sin autenticación previa, y tu AMI está confirmada como vulnerable, eso es un parche de emergencia sea el día que sea. Si requiere acceso local que un atacante solo obtendría tras haber comprometido ya un pod, tienes margen para incluir el parche en tu próxima ventana de mantenimiento programada — con prioridad, pero sin ser una alerta a las 2 de la madrugada.
Las tres formas en que existen los nodos en EKS, y por qué eso lo cambia todo
A diferencia de un Deployment, donde kubectl rollout restart funciona igual sin importar qué haya debajo, cómo parcheas un nodo depende por completo de cómo se aprovisionó ese nodo. EKS te da tres modelos distintos, y mezclarlos es el error más común que vemos cometer a los equipos durante una respuesta a una CVE.
EKS Managed Node Groups
Un Managed Node Group es el modelo en el que AWS es dueño del ciclo de vida del Auto Scaling Group subyacente y gestiona por ti la orquestación de una subida de versión. Para parchear, ejecutas aws eks update-nodegroup-version apuntando a la nueva versión de release de la AMI (o dejando que use por defecto la más reciente para tu versión de EKS). EKS entonces realiza una sustitución rodante: lanza nodos nuevos sobre la AMI parcheada, hace cordon y drain de los antiguos respetando la configuración updateConfig del node group — en concreto maxUnavailable o maxUnavailablePercentage — y termina las instancias antiguas una vez los workloads se han movido. No tocas el ASG directamente; EKS lo conduce.
El coste es que estás limitado a lo que el proceso de actualización gestionada permita o no. Respeta los PodDisruptionBudgets y detendrá el drain de un nodo si no puede desalojar un pod de forma segura, pero no te dirá mucho más allá de "actualización en curso" o "fallida" si algo se atasca, así que tener buena observabilidad sobre el scheduling de pods y el estado de los PDB durante la ventana importa.
Node groups self-managed sobre Auto Scaling Groups
Si corres nodos self-managed, tú eres dueño directamente del ASG y de la launch template. Parchear significa crear una nueva versión de la launch template con la AMI ID actualizada, actualizar el ASG para que la use como versión por defecto y luego disparar un instance refresh para realmente rotar la flota. Un instance refresh respeta un porcentaje mínimo saludable configurable y un warm-up time, lanzando instancias de reemplazo en lotes en lugar de todas a la vez — pero, a diferencia de una actualización de Managed Node Group, no tiene conciencia nativa de los PodDisruptionBudgets. Terminará una instancia en cuanto EC2 la considere "healthy", sin importar lo que kubelet esté haciendo con los pods que tiene encima, a menos que hayas integrado un Node Termination Handler o lifecycle hooks que pausen la terminación hasta que el drain termine.
Es el modelo con más pasos manuales y más formas de hacerlo mal, pero también el que da más control si necesitas algo no estándar, como parchear primero un subconjunto específico de nodos o fijar tipos de instancia junto con la subida de AMI.
Nodos provisionados por Karpenter
Karpenter invierte el modelo: no parcheas nodos directamente en absoluto, parcheas la plantilla a partir de la cual se crean. La referencia a la AMI vive en un EC2NodeClass, y la detección de drift de Karpenter compara continuamente la AMI que corre actualmente en cada nodo contra lo que el EC2NodeClass resuelve ahora. En el momento en que subes el selector de AMI — o se publica una nueva AMI que coincide con los términos de tu selector — Karpenter marca como "drifted" todos los nodos arrancados desde la AMI antigua y empieza a reemplazarlos automáticamente, aprovisionando nodos nuevos sobre la AMI parcheada y haciendo cordon de los antiguos una vez los workloads se han movido con seguridad.
El ritmo de ese reemplazo lo gobiernan los controles de disrupción de Karpenter: un ajuste budgets en el NodePool limita cuántos nodos se pueden disrumpir de forma concurrente, y la anotación karpenter.sh/do-not-disrupt en un pod (o nodo) le dice a Karpenter que deje ese nodo tranquilo incluso durante un reemplazo motivado por drift — útil para un batch job o un workload stateful a mitad de una operación que no quieres desalojar sin importar la urgencia. Es lo más parecido a "configúralo y olvídate" de los tres modelos, pero también significa que un cambio descuidado en el selector de AMI puede disparar el reemplazo de toda la flota más rápido de lo que pretendías si tu disruption budget no está lo bastante ajustado.
PodDisruptionBudgets: lo que realmente te mantiene a salvo durante el rollout
Sea cual sea de los tres modelos el que uses, el mecanismo que evita que un rollout de parche se convierta en una caída de servicio es el mismo: los PodDisruptionBudgets. Un PDB declara el número mínimo (o porcentaje) de réplicas de un workload que debe permanecer disponible durante disrupciones voluntarias, y el reemplazo de nodos — ya sea impulsado por una actualización de Managed Node Group, una instancia cordon en un instance refresh de ASG con lifecycle hooks integrados, o un reemplazo por drift de Karpenter — cuenta como disrupción voluntaria.
Cuando la API de eviction intenta drenar un pod y eliminarlo violaría el minAvailable de su PDB, la eviction se rechaza y el nodo sigue ocupado por ese pod hasta que otra réplica arranca en otro sitio y el budget lo permite. Esto es exactamente lo que quieres: es lo que impide que un parcheo rodante de AMI drene de golpe tres réplicas de un servicio de cuatro réplicas con minAvailable: 3 sobre nodos que se están terminando a la vez. También es precisamente por lo que un servicio sin ningún PDB, o un deployment de una sola réplica, es lo que se cae durante un parcheo rutinario de nodos — la eviction no tiene nada que la frene. Antes de lanzar cualquier parcheo de nodos en un cluster, comprobar que cada workload crítico tiene un PDB con un minAvailable o maxUnavailable sensato no es negociable, haya presión de CVE o no.
Haz canary del parche antes de confiárselo a toda la flota
Incluso con presión de CVE — especialmente con presión de CVE, cuando te mueves rápido y es más probable que se te escape algo — desplegar una AMI nueva directamente a todos los nodos de producción de una vez es la forma de convertir un fix de seguridad en un incidente de disponibilidad. La AMI puede tener una regresión en el networking del kernel, un cambio de configuración en containerd que rompe un sidecar, o un problema de compatibilidad con tu plugin de CNI que nadie detectó en el advisory.
La forma práctica de hacer canary depende de tu modelo de aprovisionamiento. Con Managed Node Groups, levanta un node group pequeño y separado fijado a la nueva release de AMI, etiquétalo o aplícale un taint, y programa ahí una porción representativa de workloads antes de tocar la versión del node group principal. Con Karpenter, el equivalente es un NodePool separado acotado a la nueva AMI con un taint, emparejado con una toleration en un puñado de workloads de prueba o un deployment canary, de modo que Karpenter aprovisione nodos parcheados solo para esa porción de tráfico mientras el resto de la flota se queda en la AMI probada. Dale tráfico real durante unas horas — vigila tasas de error, latencia y métricas a nivel de nodo como CPU steal o throughput de red — antes de ampliar la subida de AMI al resto de la flota. En ASGs self-managed, la misma idea funciona parcheando un ASG en una configuración de node group con varios ASGs, o fijando de forma conservadora el porcentaje mínimo saludable del instance refresh y vigilando de cerca el primer lote antes de que avance.
Managed Node Groups vs. ASGs self-managed vs. Karpenter: cómo gestiona cada uno el rollout
Rollout de parche de CVE, lado a lado
aws eks update-nodegroup-version conduce el rollout por ti, respetando maxUnavailable. El radio de impacto queda contenido por node group, pero estás limitado al ritmo que soporte la actualización gestionada. Nivel de automatización: alto en la ejecución, pero alguien tiene que dispararla por cada node group.EC2NodeClass, sin necesidad de disparo manual. El radio de impacto es lo que hay que vigilar: un ajuste budgets de disrupción demasiado laxo permite que toda la flota rote de golpe. Nivel de automatización: el más alto, precisamente por lo que el disruption budget y las anotaciones do-not-disrupt importan tanto aquí.Convertir esto en un problema de GitOps en vez de un simulacro de incendio
Ejecutar aws eks update-nodegroup-version o subir un EC2NodeClass a mano cada vez que sale una CVE funciona, pero no escala más allá de los primeros incidentes, y no deja rastro de auditoría de quién parcheó qué y cuándo. La solución es tratar la versión de la AMI — o, para las AMI optimizadas de EKS, el string de versión de release — como un valor que vive en control de versiones, no un valor que alguien teclea en un comando CLI una tarde estresante.
En la práctica eso significa que el AMI ID o la versión de release de la AMI de EKS vive en tu recurso de Terraform del node group, en tu manifiesto EC2NodeClass de Karpenter, o en el fichero de values de Helm del chart que lo gestione — y los cambios pasan por el mismo pipeline de PR-review-y-apply que cualquier otro cambio de infraestructura. Un job programado (una GitHub Action, una Lambda disparada por cron, o un bot al estilo Renovate) comprueba de forma regular si hay nuevas releases de AMI optimizadas para EKS o de Bottlerocket, abre una PR subiendo la versión fijada, y tu CI existente ejecuta terraform plan o valida automáticamente el diff del manifiesto de Karpenter. ArgoCD o Flux lo aplican después de la misma forma en que aplican cualquier otro cambio, mediante el mismo mecanismo de sync y health-check en el que ya confías, con el historial completo de cada subida de AMI en el git log en lugar de en el historial de shell de alguien.
Para CVEs genuinamente críticas, sigues queriendo tener una vía manual y expeditiva que se salte la cadencia normal y vaya directa a review-y-merge — pero es el mismo pipeline, solo que priorizado, no un proceso manual aparte que tengas que recordar cómo ejecutar bajo presión.
Conclusión
Parchear nodos de EKS ante una CVE crítica no es un único procedimiento, son tres, y elegir el equivocado — o saltarte el paso de triage que te dice si siquiera te afecta — es la forma de convertir un fix de seguridad en una caída autoinfligida. Conoce qué modelo usa cada uno de tus node groups, asegúrate de que los PodDisruptionBudgets están protegiendo de verdad tus workloads críticos antes de tocar nada, haz canary de la AMI nueva en una porción de la flota y empuja todo el proceso hacia GitOps para que la próxima CVE sea una PR, no un simulacro a las 2 de la madrugada.
¿Tu equipo tiene un plan real para la próxima CVE crítica?
Te ayudamos a mapear tus node groups, blindar los PodDisruptionBudgets e integrar el flujo de parcheo de AMI en GitOps para que no tengas que improvisar bajo presión.
Hablar con un experto →