Módulo 4: Networking Ingress And Networkpolicy
7. Manos a la obra: `NetworkPolicy` real sobre `andes-cargo`
Descripción
Esta es la lección donde el modelo deny-by-default de la lección 6 se pone a prueba contra andes-cargo-cluster de verdad — con un hallazgo que no estaba en el guion, y que vale la pena documentar con la misma honestidad que sostiene el resto de esta guía. Existe una advertencia muy repetida en tutoriales y foros sobre Kubernetes: "kind, con su CNI por defecto (kindnet), no aplica NetworkPolicy — puedes aplicar la política, no vas a recibir ningún error, y el tráfico va a seguir pasando exactamente igual, sin que nada te avise". Esa advertencia fue cierta durante años. Esta lección la verifica, en vivo, contra tu propio clúster — y el resultado real contradice la sabiduría popular. Todo lo que sigue corrió de verdad, incluida la investigación de por qué.
Conexión con el módulo
Esta lección construye, contra el clúster real, exactamente el patrón de dos políticas que la lección 6 explicó en teoría: default-deny-ingress primero, allow-from-ingress-nginx después. La diferencia frente a la lección 6 es que aquí cada afirmación se verifica con un curl real, no con un diagrama.
Paso 1 — Un Pod de prueba, para simular "cualquier otro Pod del clúster"
La lección 6 describió el problema con un Pod hipotético, "sin relación con Andes Cargo". Esta lección lo hace real: un Pod llamado test-client, en el mismo namespace andes-cargo (para simular el caso más generoso posible — ni siquiera necesitas estar en otro namespace para que el problema exista), con la única tarea de intentar curl contra status-api-service.
kubectl run test-client --image=curlimages/curl:8.11.1 -n andes-cargo --restart=Never --command -- sleep 3600
Qué esperar:
pod/test-client created
kubectl wait --for=condition=Ready pod/test-client -n andes-cargo --timeout=60s
Qué esperar:
pod/test-client condition met
Paso 2 — Confirma la línea base: sin ninguna NetworkPolicy, el acceso directo funciona
Antes de restringir nada, confirma el problema exacto que la lección 6 describió: test-client le habla directo a status-api-service, sin pasar por Ingress, sin ninguna resistencia.
kubectl exec -n andes-cargo test-client -- curl -s -o /dev/null -w "HTTP_STATUS:%{http_code}\n" --max-time 5 http://status-api-service.andes-cargo.svc.cluster.local/health
Qué esperar (literal, ejecutado):
HTTP_STATUS:200
200, directo — el mismo resultado que obtendría cualquier Pod, de cualquier namespace, sin haber pasado nunca por ingress-nginx. Esta es tu línea base: guárdala, porque el resto de esta lección se mide contra ella.
Paso 3 — La primera política: default-deny-ingress
# networkpolicy-default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: andes-cargo
spec:
podSelector: {}
policyTypes:
- Ingress
podSelector: {} —un selector vacío— es la sintaxis exacta para "todos los Pods de este namespace", sin excepción. Sin ninguna sección ingress: declarada, y con policyTypes: [Ingress] presente, el efecto es el máximo aislamiento posible: ningún Pod del namespace andes-cargo va a aceptar tráfico entrante de ningún origen, hasta que otra NetworkPolicy agregue una excepción explícita.
kubectl apply -f networkpolicy-default-deny.yaml
Qué esperar:
networkpolicy.networking.k8s.io/default-deny-ingress created
Paso 4 — Reverifica: ¿de verdad se bloqueó?
kubectl exec -n andes-cargo test-client -- curl -s -o /dev/null -w "HTTP_STATUS:%{http_code}\n" --max-time 8 http://status-api-service.andes-cargo.svc.cluster.local/health
Qué esperar (literal, ejecutado — un código 000 significa que curl nunca recibió ninguna respuesta HTTP, y el comando termina con código de salida distinto de cero):
HTTP_STATUS:000
command terminated with exit code 28
El tráfico se bloqueó — de verdad. El código de salida 28 de curl significa específicamente "tiempo de espera agotado" (CURLE_OPERATION_TIMEDOUT). Para descartar cualquier duda de que sea un problema de DNS en vez de un bloqueo de red real, repite la prueba contra la IP del Pod directamente, sin pasar por el nombre DNS del Service:
POD_IP=$(kubectl get pods -n andes-cargo -l app=andes-cargo-status-api -o jsonpath='{.items[0].status.podIP}')
kubectl exec -n andes-cargo test-client -- curl -s -o /dev/null -w "HTTP_STATUS:%{http_code}\n" --max-time 8 http://$POD_IP:8080/health
Qué esperar (literal, ejecutado — mismo resultado, contra la IP real del Pod, sin ninguna resolución DNS de por medio):
HTTP_STATUS:000
command terminated with exit code 28
Bloqueado también contra la IP directa. Esto confirma que el bloqueo ocurre en la capa de red, no en algún mecanismo de resolución de nombres — exactamente el nivel donde una NetworkPolicy real tiene que operar para ser confiable.
Paso 5 — La investigación: ¿esto no se supone que kindnet lo ignora?
Aquí es donde esta lección se detiene, porque el resultado del Paso 4 contradice una advertencia ampliamente repetida sobre kind. Durante años, la recomendación estándar de la comunidad fue clara: el CNI por defecto de kind (kindnet) no implementa el motor de aplicación de NetworkPolicy — puedes aplicar la política, kubectl apply no va a devolver ningún error (el objeto es sintácticamente válido), pero nada en el clúster la hace cumplir. El tráfico bloqueado del Paso 4 no debería haber pasado, según esa advertencia.
Investigado contra la fuente primaria (no asumido del conocimiento general), la historia completa es esta: el proyecto kind fusionó soporte de aplicación de NetworkPolicy dentro de kindnetd mismo el 23 de julio de 2024, en el pull request kubernetes-sigs/kind#3612 — construido sobre el proyecto kube-network-policies, en vez de crear un componente separado. La descripción del propio cambio lo resume así: la aplicación de políticas de red pasa a ser, literalmente, "parte de kindnetd, solo un DaemonSet distinto" — no un CNI externo que reemplaza a kindnet, sino una capacidad nueva agregada al mismo proyecto.
kubectl get daemonset kindnet -n kube-system -o jsonpath='{.spec.template.spec.containers[0].image}'
Qué esperar (literal — la etiqueta exacta de tu imagen depende de la versión de kind que instalaste en el Módulo 1; lo relevante es que corresponde a una versión publicada muy posterior a julio de 2024):
docker.io/kindest/kindnetd:v20260528-9350166c
La conclusión honesta de esta investigación: la advertencia "kind no aplica NetworkPolicy" fue cierta durante años, y sigue circulando en tutoriales y respuestas de foros que no se actualizaron — pero dejó de ser universalmente cierta desde mediados de 2024. La versión de kindnetd que instaló esta guía (Módulo 1) sí aplica NetworkPolicy de verdad, con evidencia real en el Paso 4. Esta es exactamente la razón por la que esta guía nunca repite una afirmación técnica sin verificarla contra el clúster real: una recomendación que fue correcta en su momento puede quedar obsoleta sin que nadie actualice el texto que la repite. No confíes en esta lección tampoco sin verificarla — si retomas esta guía con una versión de kind mucho más antigua que v0.32.0 (antes de julio de 2024), repite el Paso 4 tú mismo antes de asumir que el resultado va a ser el mismo.
LA LÍNEA DE TIEMPO QUE ESTA LECCIÓN VERIFICÓ
Años anteriores a jul-2024 kindnetd NO aplica NetworkPolicy
(la advertencia que sigue circulando)
│
23-jul-2024 PR #3612 fusionado: aplicación de
NetworkPolicy agregada A kindnetd
(basado en kube-network-policies)
│
Esta lección (ago-2026) Verificado en vivo, contra
andes-cargo-cluster: kindnetd
SÍ bloquea el tráfico real
Paso 6 — Sobre Calico, y por qué esta lección no lo instala
Antes del hallazgo del Paso 5, esta guía esperaba tener que instalar Calico —el CNI de terceros más citado como reemplazo para obtener aplicación real de NetworkPolicy en kind— para poder demostrar un bloqueo genuino. La investigación en vivo cambió ese plan, y vale la pena explicar por qué, en vez de instalarlo de todas formas sin necesidad real.
Calico sigue siendo, hoy, una elección extremadamente común en clústeres de producción reales —incluido EKS, donde el CNI por defecto (Amazon VPC CNI, Módulo 7) históricamente tuvo soporte más limitado de NetworkPolicy que un CNI dedicado como Calico—, y ofrece capacidades que van más allá de lo que el objeto estándar NetworkPolicy de Kubernetes cubre: GlobalNetworkPolicy (reglas a nivel de todo el clúster, no por namespace), políticas con prioridad explícita y reglas de "denegar" verdaderas (a diferencia del modelo puramente aditivo que explicó la lección 6), y un plano de datos basado en eBPF para clústeres de alto rendimiento. Instalar Calico exigiría, además, recrear el clúster una segunda vez en esta misma lección (disableDefaultCNI: true es, igual que extraPortMappings, configuración que solo se declara al crear el clúster — la misma restricción que ya viste en la lección 4) — un costo real, sin ninguna ganancia pedagógica, porque el objetivo de esta lección (bloqueo real y verificado) ya está cumplido con evidencia directa.
Esta guía prefiere la honestidad de un resultado inesperado, bien investigado y bien citado, sobre instalar una pieza adicional solo para confirmar algo que el clúster ya demostró por su cuenta.
Paso 7 — La segunda política: permite solo el tráfico de ingress-nginx
Con el bloqueo confirmado como real, agrega la excepción explícita que necesita status-api-service para seguir funcionando a través de la puerta que las lecciones 4-5 construyeron:
# networkpolicy-allow-ingress-nginx.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-ingress-nginx
namespace: andes-cargo
spec:
podSelector:
matchLabels:
app: andes-cargo-status-api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 8080
Tres decisiones, cada una deliberada:
podSelector: app: andes-cargo-status-api, no{}— esta política solo afecta a los Pods de Andes Cargo, no a todo el namespace (útil en cuanto el namespace tenga más de un servicio, algo que esta guía no construye pero que cualquier clúster real termina teniendo).namespaceSelector: kubernetes.io/metadata.name: ingress-nginx—kubernetes.io/metadata.namees una etiqueta que Kubernetes agrega automáticamente a todo namespace desde la versión 1.21, con el nombre exacto del namespace como valor. Es la forma recomendada de seleccionar un namespace por nombre en unaNetworkPolicy, sin depender de que alguien haya etiquetado ese namespace a mano.ports: 8080/TCP— la excepción se limita al puerto exacto donde escucha el contenedor (targetPortenservice.yaml, Módulo 2), no a "cualquier puerto desde ese namespace". Menor privilegio, aplicado hasta el último detalle disponible.
kubectl apply -f networkpolicy-allow-ingress-nginx.yaml
Qué esperar:
networkpolicy.networking.k8s.io/allow-from-ingress-nginx created
Paso 8 — Verifica los dos caminos, uno por uno
El camino permitido — a través de Ingress:
curl -s -o /dev/null -w "HTTP_STATUS:%{http_code}\n" --max-time 8 --resolve andes-cargo.local:80:127.0.0.1 http://andes-cargo.local/health
Qué esperar (literal, ejecutado):
HTTP_STATUS:200
El camino que sigue bloqueado — test-client directo, saltándose Ingress:
kubectl exec -n andes-cargo test-client -- curl -s -o /dev/null -w "HTTP_STATUS:%{http_code}\n" --max-time 8 http://status-api-service.andes-cargo.svc.cluster.local/health
Qué esperar (literal, ejecutado — sin cambios frente al Paso 4, porque test-client corre en el namespace andes-cargo, no en ingress-nginx):
HTTP_STATUS:000
command terminated with exit code 28
Este par de resultados es la prueba completa del patrón de dos políticas que explicó la lección 6: el tráfico que viene de ingress-nginx —el único camino legítimo, según el diseño de este módulo— pasa; cualquier otro Pod, incluido uno del mismo namespace que Andes Cargo, sigue completamente bloqueado. Confirma el estado final de ambas políticas:
kubectl get networkpolicy -n andes-cargo
Qué esperar (literal, ejecutado — AGE es tu valor variable):
NAME POD-SELECTOR AGE
allow-from-ingress-nginx app=andes-cargo-status-api 22s
default-deny-ingress <none> 2m53s
kubectl describe networkpolicy allow-from-ingress-nginx -n andes-cargo
Qué esperar (literal, ejecutado):
Name: allow-from-ingress-nginx
Namespace: andes-cargo
Created on: 2026-08-14 14:42:01 -0600 CST
Labels: <none>
Annotations: <none>
Spec:
PodSelector: app=andes-cargo-status-api
Allowing ingress traffic:
To Port: 8080/TCP
From:
NamespaceSelector: kubernetes.io/metadata.name=ingress-nginx
Not affecting egress traffic
Policy Types: Ingress
Not affecting egress traffic —la última línea— confirma exactamente lo que anticipó la lección 6: esta política, al declarar solo policyTypes: [Ingress], no toca en absoluto la capacidad de andes-cargo-status-api de iniciar conexiones hacia afuera (por ejemplo, hacia una base de datos externa como LocalStack, si alguien la cableara). El 500 de /shipments/<id> sigue existiendo por la misma causa de siempre —ningún módulo de esta guía instala LocalStack dentro del clúster—, no por ninguna restricción nueva de esta lección.
Analogía: las cerraduras instaladas, probadas con la llave equivocada primero
Retomando la analogía del edificio: esta lección instaló, de verdad, las cerraduras en cada puerta interior de la oficina de Andes Cargo (default-deny-ingress), y las probó de la única forma confiable — intentando entrar con alguien que no tiene la tarjeta correcta (test-client), y confirmando que la puerta de verdad no se abre. Solo después de esa prueba, se le entregó la tarjeta correcta exactamente a quien debería tenerla: el recepcionista del edificio (ingress-nginx), y solo a él. Cualquier otro residente del edificio —incluido uno que vive en el mismo piso que Andes Cargo— sigue sin poder entrar, aunque esté físicamente cerca.
Errores comunes
Repetir "kind no aplica NetworkPolicy" sin haberlo verificado contra tu propia versión (el error que esta lección corrige de raíz, y el más importante de prevenir hacia adelante). Qué pasa: alguien lee esa advertencia en un tutorial o una respuesta de foro —muy probablemente escrita antes de julio de 2024— y la repite como un hecho permanente, sin considerar que el propio proyecto kind cambió ese comportamiento. Cómo detectarlo: si estás a punto de instalar Calico "porque kind no soporta esto", sin haber corrido primero el Paso 4 de esta lección contra tu propio clúster. Cómo corregirlo: verifica siempre, con el patrón exacto de esta lección (línea base, aplicar la política, reverificar), antes de asumir cualquier comportamiento de infraestructura que dependa de una versión específica de una herramienta que cambia con el tiempo.
Olvidar que default-deny-ingress también bloquea el tráfico legítimo de ingress-nginx, hasta que se agrega la segunda política (de secuencia, ya anticipado en el Paso 8 pero fácil de pasar por alto si saltas pasos). Qué pasa: alguien aplica solo default-deny-ingress y, al probar curl a través de Ingress, ve que también falla, y piensa que algo salió mal con ingress-nginx mismo. Cómo detectarlo: si el curl vía --resolve andes-cargo.local falla justo después del Paso 3, antes de haber llegado al Paso 7. Cómo corregirlo: es el comportamiento esperado — default-deny-ingress no hace ninguna excepción por defecto, ni siquiera para el tráfico que viene de tu propio Ingress. La segunda política (Paso 7) es la que restaura ese camino específico.
Confundir el código 000/exit code 28 con un error de la aplicación (de diagnóstico, distingue esta lección de la 5). Qué pasa: alguien ve que curl no devuelve ningún código HTTP y busca el problema en los logs de andes-cargo-status-api, esperando encontrar un traceback como los del Módulo 3. Cómo detectarlo: kubectl logs de los Pods de andes-cargo-status-api no muestra ninguna entrada nueva correspondiente al intento bloqueado — la solicitud nunca llegó a la aplicación. Cómo corregirlo: un código 000 con curl significa que la conexión nunca se completó a nivel de red — el paquete fue descartado antes de llegar al proceso Flask. Esa ausencia total de logs en la aplicación es, en sí misma, la prueba de que el bloqueo ocurre en una capa anterior (la NetworkPolicy, aplicada por kindnetd), no dentro del contenedor.
Ejercicios
Ejercicio 1 — Reconstruye la investigación completa de memoria. Sin volver a la lección, describe, en orden: (a) qué advertencia repite la comunidad sobre kind y NetworkPolicy; (b) qué encontraste al verificarlo contra tu clúster; (c) la fuente exacta (PR y fecha) que explica la discrepancia.
Ver solución
(a) Que kindnet, el CNI por defecto de kind, no aplica NetworkPolicy — la política se aceptaría sin error, pero no bloquearía nada. (b) Al aplicar default-deny-ingress contra andes-cargo-cluster real, el tráfico sí se bloqueó (curl con código 000, exit code 28, verificado contra el Service y contra la IP del Pod directamente). (c) El pull request kubernetes-sigs/kind#3612, fusionado el 23 de julio de 2024, que agregó aplicación de NetworkPolicy directamente a kindnetd, basado en el proyecto kube-network-policies.
Ejercicio 2 — Explica por qué probar con la IP del Pod, no solo con el nombre DNS, importaba. Un colega pregunta por qué esta lección repitió la prueba del Paso 4 contra la IP directa del Pod, si ya había fallado contra el nombre DNS del Service. Explica, en dos o tres frases, qué duda descarta esa segunda prueba.
Ver solución
Una explicación razonable: "Un curl que falla contra un nombre DNS podría fallar por dos razones muy distintas: que CoreDNS no resuelva el nombre (un problema de DNS, no de bloqueo de red — exactamente lo que pasó en el Módulo 3 con LocalStack), o que la conexión de red misma esté bloqueada. Repetir la prueba contra la IP del Pod directamente elimina la resolución de nombres de la ecuación por completo — si sigue fallando contra una IP real, la única explicación posible es un bloqueo a nivel de red, que es exactamente lo que una NetworkPolicy debería producir."
Ejercicio 3 — Predice qué pasaría si test-client corriera en el namespace ingress-nginx. Si, en vez de crear test-client en el namespace andes-cargo, lo hubieras creado dentro del namespace ingress-nginx, ¿esperarías que curl contra status-api-service funcionara, después de aplicar ambas políticas de esta lección? Justifica tu respuesta con la regla exacta de allow-from-ingress-nginx.
Ver solución
Sí, funcionaría — la regla allow-from-ingress-nginx permite tráfico desde cualquier Pod cuyo namespace tenga la etiqueta kubernetes.io/metadata.name: ingress-nginx, sin ninguna restricción adicional sobre qué Pod específico dentro de ese namespace. Esto revela una limitación real del diseño de esta lección, útil para entender el modelo a fondo: la política confía en el namespace de origen, no en la identidad específica del Pod (ingress-nginx-controller, y ningún otro). Un Pod cualquiera, sin relación con el controlador real, colocado deliberadamente dentro del namespace ingress-nginx (algo que requeriría permisos para crear objetos ahí, una capa de seguridad distinta —RBAC— fuera del alcance de esta guía), también pasaría la política. En un clúster de producción real, esa capa de RBAC es exactamente lo que evita que cualquiera pueda simplemente "mudarse" al namespace correcto para saltarse una NetworkPolicy basada en namespaceSelector.
Resumen y siguiente paso
Esta lección construyó, contra andes-cargo-cluster real, el patrón completo de dos políticas de la lección 6 —default-deny-ingress seguido de allow-from-ingress-nginx—, con cada afirmación verificada con curl real: acceso directo abierto antes de cualquier política (200), bloqueado por completo después de default-deny-ingress (000, verificado contra el Service y contra la IP del Pod), y restaurado solo para el camino legítimo después de la segunda política (Ingress → 200; cualquier otro Pod → sigue bloqueado). En el camino, esta lección corrigió una advertencia ampliamente repetida sobre kind: desde julio de 2024, kindnetd sí aplica NetworkPolicy de verdad, un hallazgo verificado en vivo y citado contra su fuente primaria, no asumido de la sabiduría popular.
Antes de avanzar deberías poder: reconstruir el patrón completo de dos políticas contra un clúster real; explicar por qué la advertencia sobre kind y NetworkPolicy dejó de ser universalmente cierta, con la fecha y el PR exactos; y diagnosticar un bloqueo de NetworkPolicy real (código 000, sin logs de aplicación) frente a un error de la propia aplicación.
Siguiente lección: proyecto de este módulo, status-api-service expuesto y protegido. Ahí se juntan Ingress (lecciones 4-5) y NetworkPolicy (lecciones 6-7) en la misma verificación final: el camino permitido, y el camino bloqueado, confirmados los dos, uno junto al otro.
Recursos
- Kubernetes — Network Policies — la misma referencia de la lección 6, ahora confirmada con evidencia real de bloqueo y permiso.
- GitHub — kubernetes-sigs/kind, PR #3612: Kindnet Network Policies — la fuente primaria exacta de la investigación de esta lección: fusionado el 23 de julio de 2024.
- Kubernetes — Namespaces: Automatic Labelling — referencia oficial de la etiqueta
kubernetes.io/metadata.name, usada enallow-from-ingress-nginx. - Project Calico — el CNI nombrado por contraste en el Paso 6, con soporte extendido (
GlobalNetworkPolicy) más allá delNetworkPolicyestándar de Kubernetes. - Calico — Network Policy — documentación oficial del modelo de políticas extendido de Calico, para quien quiera profundizar más allá del alcance de esta guía.