Unos 800 desarrolladores pasaron semanas sin poder desplegar a producción porque sus pods no conseguían abrir una conexión TCP con su propio API server. Detrás no había un fallo, sino tres, en tres capas distintas, y el arreglo definitivo acabó siendo un parche del kernel de Linux. Google lo contó el 30 de octubre de 2026 en su blog, y es un postmortem que merece la pena aunque nunca hayas tocado Private Service Connect.
El síntoma: timeouts contra la VIP del API server
El cliente tenía un entorno grande de GKE multiclúster en europe-west, con el plano de control expuesto de forma privada mediante Private Service Connect (PSC). En enero de 2026 empezaron a aparecer errores dial tcp ...:443: i/o timeout contra la IP interna del kube-apiserver en un clúster de UAT. El día 10 ya eran persistentes; el día 15 se abrió un bug P0 con cinco clústeres afectados y los despliegues a producción congelados.
Lo interesante es la lista de sospechosos descartados, porque es la que cualquiera habría seguido: un operador propio escrito con kopf (los errores empezaron ocho días antes de desplegarlo), External Secrets Operator y los agentes de seguridad (desinstalados, sin cambios), agotamiento de conntrack (al 50 % de 1,3 millones de entradas) y un plano de control saturado (se agrandó la VM y las latencias ya eran buenas). Fallaban DaemonSets, Deployments, pods de sistema y también clientes externos como portátiles y runners de GitHub Actions. El problema estaba en el camino, no en ninguna carga.
Lo que enseñaron las capturas
Con capturas simultáneas en la NIC de la VM del plano de control y dentro del contenedor del API server apareció el patrón: el pod enviaba un SYN por un puerto de origen que se había usado minutos antes y, en lugar de un SYN-ACK, recibía un ACK que pertenecía a la conexión anterior. El servidor seguía con ese socket en FIN_WAIT_2 más de dos minutos después de cerrarlo.
Ese ACK es el challenge ACK del RFC 5961: cuando llega un SYN que coincide con una conexión que el kernel aún recuerda, responde con el número de secuencia antiguo para que el cliente legítimo conteste con un RST y limpie el estado. Funciona si el ACK llega. Un cambio en el conmutador de paquetes de PSC, desplegado para corregir un problema de seguridad distinto en la cabecera proxy protocol, empezó a descartar los ACK que llegan antes del SYN-ACK. Un contador interno de "invalid ACK" que llevaba meses plano saltó a varios descartes por segundo el 18 de febrero de 2026, coincidiendo con el despliegue.
El NAT de PSC agravaba todo: muchos pods de nodos distintos comparten el endpoint y acaban reutilizando el mismo puerto de origen, así que las colisiones de 5-tupla, y con ellas los challenge ACK, eran mucho más frecuentes que en una conexión directa.
Tres fallos que solo juntos tumbaban el clúster
Retirar el cambio de PSC en febrero devolvió la normalidad a principios de marzo, pero dejaba sin responder la pregunta importante: ¿por qué había sockets eternos en FIN_WAIT_2?
- Reordenación de paquetes. El primer paquete de un flujo nuevo pasa por un camino lento en PSC (NAT y cabecera proxy) y los siguientes por uno rápido. En la NIC del host los paquetes llegaban en orden; en la NIC virtual de la VM, no. El FIN del cliente podía llegar después de ACK posteriores.
- El half-close de Envoy. Envoy, delante del kube-apiserver, cerraba con
shutdown(SHUT_WR): deja de escribir pero mantiene el socket abierto para leer. Es TCP correcto, perotcp_fin_timeout(60 segundos en esas VM) solo se aplica a sockets huérfanos. Con Envoy sujetándolo, el temporizador nunca arrancaba. - Un bug del kernel 6.1. Al llegar el FIN reordenado,
tcp_ack()devolvía 0 para un ACK antiguo pero aceptable según el RFC 5961, y enFIN_WAIT_2eso hacía quetcp_fin()no se llamara nunca. El FIN se descartaba en silencio.
Por separado, cada uno era tolerable. Juntos, el FIN llegaba desordenado, el kernel lo ignoraba, Envoy impedía el timeout, el NAT reutilizaba el puerto y PSC tiraba el challenge ACK que lo habría arreglado.
Cómo se arregló
Además del rollback, PSC recibió un cambio que trata bien el caso del challenge ACK, con un test de PacketDrill para que no vuelva. La causa raíz se corrigió en el kernel con el commit "net: tcp: accept old ack during closing", de Eric Dumazet: está en Linux 6.9.7, se ha llevado a las ramas LTS 6.6.y y 6.1.y y llega a GKE con las imágenes de COS de la versión 1.33.5 en adelante.
Lo que me llevo
La lección que más me sirve fuera de Google es la del half-close: tcp_fin_timeout no te protege de un proxy que mantiene el socket abierto. Si operas Envoy, HAProxy o tu propio servicio detrás de NAT y ves timeouts esporádicos al conectar, mira cuántos sockets llevan tiempo en FIN_WAIT_2 y si hay un proceso detrás:
# Sockets en FIN_WAIT_2 y el proceso que los mantiene (requiere root)
ss -tanp state fin-wait-2
# Cuántos hay
ss -tan state fin-wait-2 | tail -n +2 | wc -lSi la cuenta no deja de crecer y cada socket tiene un proceso asociado, tcp_fin_timeout no los va a cerrar. Y si tus nodos van con un kernel 6.1 o 6.6 propio, comprueba que tu distribución incluye la corrección de tcp_ack().
La segunda: cualquier middlebox que "limpie" paquetes raros puede romper mecanismos de TCP que nadie recuerda que existen hasta que fallan. Aquí bastó con que un filtro bien intencionado lo considerara inválido.
La tercera es de método: el caso solo se resolvió con capturas simultáneas en el host, en la NIC virtual y dentro del contenedor, cruzadas con las métricas de la red. Una captura en un solo punto habría enseñado el síntoma, nunca la reordenación.
Qué no es trasladable
USPS, Andromeda y la NIC virtual de Google son piezas internas: no vas a poder instrumentarlas ni parchearlas, y en GKE el plano de control lo actualiza Google. El caso es además de un único cliente anonimizado, con un patrón de reutilización de puertos que depende de cómo PSC comparte el endpoint. Lo que sí viaja es el mecanismo: reordenación más half-close más un kernel sin el parche puede dejarte sockets colgados en cualquier proxy propio.
Fuentes
- When the Kubernetes API went silent: A three-layer networking mystery, Google Cloud Blog, 30 de octubre de 2026.