Cerramos el módulo con la lección que arregla el agujero que arrastramos desde el módulo 3: cualquier pod del clúster puede conectarse a postgres-reservas:5432. Un pod comprometido, una dependencia maliciosa en una imagen o un despliegue equivocado en rutas-norte-dev tienen hoy acceso directo a la base de datos con el nombre, el DNI, el teléfono y el correo de todos los clientes de Rutas Norte. Las NetworkPolicies son el cortafuegos nativo de Kubernetes, y en esta lección construiremos el modelo de aislamiento de la plataforma paso a paso: denegar todo, abrir el DNS —el fallo que tumba el clúster entero y que casi todo el mundo comete una vez—, y autorizar una por una únicamente las conversaciones que la plataforma necesita, verificando cada regla con pods efímeros.
Contenido
- Demostrar el problema: la red es plana
- El requisito imprescindible: un CNI que las implemente
- Anatomía de una NetworkPolicy
- El modelo aditivo: solo se permite, nunca se deniega
from/to:podSelector,namespaceSelectoreipBlock- El error más caro: Y frente a O
- Paso 1: denegar todo en
rutas-norte-pro - Paso 2: permitir DNS (o romperlo todo)
- Paso 3: las conversaciones de la plataforma
- Paso 4: el Ingress y la pasarela de pagos externa
- Verificación sistemática
- Limitaciones reales
- Demostrar el problema: la red es plana
Antes de arreglar nada, veámoslo. Lanzamos en producción un pod que no tiene ninguna relación con la plataforma: sin etiquetas de Rutas Norte, sin ServiceAccount dedicada, sin nada.
kubectl run intruso --rm -it --restart=Never -n rutas-norte-pro \
--image=nicolaka/netshoot -- bash
# Y desde dentro del pod intruso, todo responde:
nc -zv postgres-reservas 5432 # la base de datos con los datos personales
nc -zv redis-cache 6379 # la cache
nc -zv postgres-reservas.rutas-norte-dev 5432 # OTRO entorno: los namespaces no aislan
curl -s -o /dev/null https://pagos.proveedorexterno.example/ # y salida a internetTodo funciona. Es la regla 2 del modelo de red de 04-01: todo pod puede hablar con todo pod sin NAT. No es un fallo: es el comportamiento especificado. Y sus implicaciones son serias:
| Escenario | Consecuencia hoy |
|---|---|
Se explota una vulnerabilidad en tienda-web (nginx expuesto a internet) |
Desde ese pod se llega directamente a la base de datos |
Una dependencia de worker-notificaciones resulta maliciosa |
Puede exfiltrar datos a cualquier destino de internet |
Una prueba mal configurada en dev |
Puede escribir en postgres-reservas de producción |
Ya hemos puesto capas —identidad mínima con ServiceAccounts (03-06) y cuotas por entorno (03-04)—; falta la capa de red.
- El requisito imprescindible: un CNI que las implemente
Aquí está el peligro silencioso de esta lección. Kubernetes define el objeto NetworkPolicy, pero no lo aplica. Quien lo aplica es el plugin CNI, y si el plugin no lo implementa el objeto se crea sin ningún error ni aviso, kubectl get networkpolicy lo lista con normalidad, kubectl describe muestra las reglas perfectamente… y el tráfico sigue pasando exactamente igual. Tendrás una política de seguridad que no protege nada y una falsa sensación de estar cubierto. Como vimos en 04-01, Flannel no implementa NetworkPolicy, y es el CNI por defecto de muchísimas instalaciones de laboratorio, incluida la de minikube.
Comprobar qué CNI tienes y activar Calico
kubectl get pods -n kube-system -o name | grep -Ei "flannel|calico|cilium|weave"
# kube-flannel-ds-h4k2p -> Flannel: las politicas NO van a hacer nada
# Perfil nuevo con Calico (el CNI no se cambia en caliente con seguridad)
minikube start -p rutas-norte --cni=calico \
--addons=ingress,metrics-server,storage-provisioner
kubectl get pods -n kube-system -l k8s-app=calico-nodeAlternativa: --cni=cilium, que además da políticas L7 y observabilidad con Hubble. Pero nunca confíes en el nombre del plugin: verifica con una prueba real (ejercicio 1). La idea es crear un pod diana en un namespace de usar y tirar, comprobar que responde, aplicar un deny-all de ingress y comprobar que deja de responder; si sigue respondiendo, tu CNI ignora las políticas. Hazlo siempre en un clúster nuevo, antes de escribir una sola política de verdad.
- Anatomía de una NetworkPolicy
apiVersion: networking.k8s.io/v1 # v1 estable; no uses extensions/v1beta1
kind: NetworkPolicy
metadata:
name: ejemplo
namespace: rutas-norte-pro # SIEMPRE tiene namespace
spec:
podSelector: # 1. A QUE PODS se aplica (en su namespace)
matchLabels: { app: postgres-reservas }
policyTypes: [Ingress, Egress] # 2. QUE DIRECCIONES regula
ingress: # 3. Reglas de entrada
- from:
- podSelector:
matchLabels: { app: api-reservas }
ports:
- { protocol: TCP, port: 5432 }
egress: # 4. Reglas de salida
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
ports:
- { protocol: UDP, port: 53 }| Campo | Significado | Detalle crítico |
|---|---|---|
podSelector |
Los pods protegidos por esta política | {} (vacío) significa todos los pods del namespace |
policyTypes |
Qué direcciones regula | Si omites Egress, la salida no se restringe |
ingress[].from / egress[].to |
Orígenes permitidos para entrar / destinos permitidos para salir | Listas; cada elemento es un "O" |
ports |
Puertos y protocolos permitidos | Si se omite, todos los puertos |
Tres avisos sobre policyTypes, que es donde más se falla: si lo omites, Kubernetes lo infiere (incluye Ingress siempre y Egress solo si hay bloque egress), así que escríbelo explícitamente; policyTypes: [Ingress] con ingress: [] significa "denegar toda la entrada", distinto de no tener el campo; y policyTypes: [Ingress, Egress] sin reglas es el aislamiento total. Y el punto clave: una NetworkPolicy tiene namespace y su podSelector solo mira dentro del suyo. Para proteger los tres entornos hacen falta las políticas en los tres.
- El modelo aditivo: solo se permite, nunca se deniega
El modelo mental que hay que fijar antes de escribir nada:
flowchart LR
A["Alguna NetworkPolicy selecciona<br/>a este pod en esta direccion?"] -->|NO| B["TODO PERMITIDO<br/>(el pod no esta aislado)"]
A -->|SI| C["Pod AISLADO:<br/>por defecto se deniega todo"]
C --> D["Alguna de esas politicas<br/>permite este trafico?"]
D -->|"SI (basta UNA)"| E["PERMITIDO"]
D -->|NO| F["DENEGADO"]
De ahí salen cuatro reglas de oro: sin políticas, todo pasa (el estado de Rutas Norte hasta ahora); en cuanto UNA política selecciona a un pod en una dirección, ese pod queda aislado en esa dirección y solo pasa lo explícitamente permitido; las políticas son aditivas y no existe "denegar" —no hay campo deny, la unión de todas las que seleccionan al pod define lo permitido y basta con que una lo autorice—; y no hay prioridades ni orden, ni order ni "la última gana".
Consecuencia práctica: no puedes hacer una excepción restrictiva. Si una política permite a todo rutas-norte-pro llegar a postgres-reservas, no puedes añadir otra que diga "menos este pod": hay que eliminar el permiso amplio y enumerar los permitidos. Otro detalle que confunde: ingress y egress se evalúan por separado y en ambos extremos, así que para que api-reservas hable con postgres-reservas hacen falta dos autorizaciones —el egress del emisor y el ingress del receptor—, y si falta cualquiera no hay conexión; es la causa número uno de "he permitido el tráfico y sigue sin funcionar". Por último, las políticas actúan sobre conexiones y los CNI que las implementan siguen el estado, así que la respuesta vuelve sin necesitar una regla de vuelta.
from/to: podSelector, namespaceSelector e ipBlock
from/to: podSelector, namespaceSelector e ipBlockHay exactamente tres formas de identificar el otro extremo.
podSelector: pods del MISMO namespace
Un podSelector dentro de from/to no puede alcanzar otros namespaces; para eso está el siguiente.
namespaceSelector: todos los pods de ciertos namespaces
Kubernetes añade a cada namespace la etiqueta kubernetes.io/metadata.name con su nombre, lo que evita etiquetarlos a mano; aun así conviene poner etiquetas propias y estables (kubectl label namespace rutas-norte-pro entorno=pro).
ipBlock: rangos CIDR, para lo que está fuera del clúster
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0 # todo internet...
except: [10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16] # ...menos redes privadas
ports:
- { protocol: TCP, port: 443 }Dos advertencias: no sirve para pods —aunque técnicamente una IP de pod esté en un CIDR, la implementación lo trata como tráfico externo y no es fiable; para pods, selectores— y except solo resta del cidr de ese mismo bloque, no es una denegación global. El patrón 0.0.0.0/0 con except de las redes privadas es el estándar para decir "puede salir a internet, pero no puede pivotar hacia la red interna de la empresa".
- El error más caro: Y frente a O
Este apartado es el que más incidentes de seguridad reales evita. Comparemos dos fragmentos que se diferencian en un guion:
# VERSION A -- OJO: esto es un O (union)
ingress:
- from:
- podSelector:
matchLabels: { app: api-reservas }
- namespaceSelector: # <-- guion propio: OTRO elemento de la lista
matchLabels: { entorno: pro }
# VERSION B -- esto es un Y (interseccion)
ingress:
- from:
- podSelector:
matchLabels: { app: api-reservas }
namespaceSelector: # <-- SIN guion: mismo elemento
matchLabels: { entorno: pro }| Versión A (dos guiones) | Versión B (un guion) | |
|---|---|---|
| Semántica | podSelector O namespaceSelector |
podSelector Y namespaceSelector |
| Quién entra | Pods app=api-reservas de este namespace, más TODOS los pods de cualquier namespace con entorno=pro |
Solo los pods app=api-reservas que estén en un namespace entorno=pro |
| Riesgo | Enorme: cualquier pod de producción entra a la base de datos | Correcto |
La versión A abre la base de datos a namespaces enteros. Es un fallo que se cuela con facilidad en una revisión de código porque el YAML parece decir lo contrario, y el clúster no da ningún aviso: la política se aplica y funciona, solo que permite mucho más de lo previsto. Regla mnemotécnica: cada guion de la lista from es un "O"; los campos dentro de un mismo elemento son un "Y". La misma distinción aplica a los elementos de ingress (lista de reglas unidas por O) y a ports dentro de una regla.
- Paso 1: denegar todo en
rutas-norte-pro
rutas-norte-proEmpezamos por el cimiento: cerrar todo, y después abrir lo justo, aunque durante unos minutos la plataforma quede rota.
# k8s/entornos/pro/np-00-deny-all.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: rutas-norte-pro
labels: { app.kubernetes.io/part-of: rutas-norte, entorno: pro }
spec:
podSelector: {} # {} = TODOS los pods del namespace
policyTypes: [Ingress, Egress]
# sin bloques ingress ni egress = no se permite nadakubectl apply -f k8s/entornos/pro/np-00-deny-all.yaml
kubectl exec -n rutas-norte-pro deploy/api-reservas -- \
timeout 5 nc -zv postgres-reservas 5432 || echo "DENEGADO (esperado)"La plataforma está completamente rota, y es lo que queríamos: a partir de aquí cada regla será una decisión consciente y documentada. Es el mínimo privilegio aplicado a la red. Nota importante: kubectl exec, kubectl logs y las sondas de salud del kubelet (07-01) no se ven afectados, porque vienen del nodo y no de otro pod; por eso puedes seguir depurando con un clúster totalmente cerrado.
- Paso 2: permitir DNS (o romperlo todo)
Este es el fallo clásico de las NetworkPolicies, y merece apartado propio porque su síntoma despista muchísimo. Con deny-all activo ningún pod puede hablar con CoreDNS, y en los logs de api-reservas se ve esto:
Error: getaddrinfo EAI_AGAIN postgres-reservas
Error: getaddrinfo EAI_AGAIN pagos.proveedorexterno.exampleEl síntoma no es "conexión rechazada": es un fallo de resolución de nombres tras varios segundos de espera, el fallo tipo 1 de 04-03. La confusión típica es investigar CoreDNS, que está perfectamente sano, en lugar de mirar la política de egress recién aplicada. Agravante: un deny-all de egress en kube-system deja sin DNS a todo el clúster; nunca apliques políticas amplias en los namespaces del sistema.
# k8s/entornos/pro/np-01-allow-dns.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-dns-egress, namespace: rutas-norte-pro }
spec:
podSelector: {} # todos los pods del namespace necesitan DNS
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
podSelector: # SIN guion: Y logico -> CoreDNS DENTRO de kube-system
matchLabels: { k8s-app: kube-dns }
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 } # imprescindible: respuestas grandes, NodeLocal DNSCacheTres detalles que hay que respetar:
podSelector: {}: el DNS lo necesitan todos los pods, sin excepción. Es la primera regla que se escribe siempre.- UDP y TCP en el 53. Se olvida el TCP constantemente. El DNS cae a TCP cuando la respuesta no cabe en un paquete UDP, y en un Service headless con muchos pods eso pasa: el síntoma es demoledor, funciona casi siempre y falla de forma aparentemente aleatoria.
namespaceSelector+podSelectorsin guion, para que sea la intersección: CoreDNS dentro dekube-system. Con guion abrirías todokube-system. Verifica además la etiqueta real de esos pods (kubectl get pods -n kube-system --show-labels | grep dns); en algunas distribuciones esk8s-app: coredns.
kubectl apply -f k8s/entornos/pro/np-01-allow-dns.yaml
kubectl exec -n rutas-norte-pro deploy/api-reservas -- nslookup postgres-reservas
# resuelve correctamente
kubectl exec -n rutas-norte-pro deploy/api-reservas -- timeout 5 nc -zv postgres-reservas 5432
# sigue DENEGADO: resolver no es conectar ([04-03](04-03-dns-interno-y-descubrimiento-de-servicios))
- Paso 3: las conversaciones de la plataforma
Enumeramos, una a una, las conexiones legítimas de Rutas Norte:
| Origen | Destino | Puerto | Motivo |
|---|---|---|---|
api-reservas |
postgres-reservas |
5432 | Consultar disponibilidad y crear reservas |
worker-notificaciones |
postgres-reservas |
5432 | Leer los datos de la reserva para el correo |
api-reservas |
redis-cache |
6379 | Caché de disponibilidad de plazas |
| Controlador de Ingress | tienda-web |
8080 | Tráfico público de la tienda |
| Controlador de Ingress | api-reservas |
3000 | Tráfico público de la API |
api-reservas |
Pasarela de pagos | 443 | Cobrar los billetes |
| Todos | CoreDNS | 53 | Ya autorizado |
Nada más: tienda-web no habla con la base de datos (sirve una SPA estática), redis-cache no inicia conexiones y postgres-reservas no sale a ningún sitio.
La base de datos: solo dos clientes
# k8s/entornos/pro/np-02-postgres.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: postgres-reservas-ingress, namespace: rutas-norte-pro }
spec:
podSelector:
matchLabels: { app: postgres-reservas } # solo app y entorno en los selectores
policyTypes: [Ingress]
ingress:
- from:
- podSelector: { matchLabels: { app: api-reservas } } # guion 1
- podSelector: { matchLabels: { app: worker-notificaciones } } # guion 2 (O)
ports:
- { protocol: TCP, port: 5432 }
---
# k8s/entornos/pro/np-03-api-reservas-egress.yaml -- y ahora el lado del EMISOR,
# que es la mitad que se olvida: sin este egress, la conexion tampoco se establece
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: api-reservas-egress, namespace: rutas-norte-pro }
spec:
podSelector:
matchLabels: { app: api-reservas }
policyTypes: [Egress]
egress:
- to: [ { podSelector: { matchLabels: { app: postgres-reservas } } } ]
ports: [ { protocol: TCP, port: 5432 } ]
- to: [ { podSelector: { matchLabels: { app: redis-cache } } } ]
ports: [ { protocol: TCP, port: 6379 } ]La primera es la política que justifica toda la lección: ningún otro pod del clúster puede ya intentar abrir una conexión a la base de datos con los datos personales. Y recuerda que allow-dns-egress y la segunda se suman: api-reservas sale al DNS, a PostgreSQL y a Redis, y a nada más. El egress de worker-notificaciones es análogo, con PostgreSQL y el correo corporativo.
- Paso 4: el Ingress y la pasarela de pagos externa
Desde el controlador de Ingress
El controlador vive en ingress-nginx, así que necesitamos namespaceSelector:
# k8s/entornos/pro/np-04-desde-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-ingress-controller, namespace: rutas-norte-pro }
spec:
podSelector:
matchExpressions:
- { key: app, operator: In, values: ["tienda-web", "api-reservas"] }
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: ingress-nginx }
ports:
- { protocol: TCP, port: 8080 } # targetPort de tienda-web
- { protocol: TCP, port: 3000 } # targetPort de api-reservasDos detalles: los puertos son los del contenedor (targetPort), no los del Service, porque las políticas ven el tráfico real que llega al pod y kube-proxy ya tradujo el port en el nodo de origen (04-01) —poner el 80 en lugar del 8080 produce un 503 en el Ingress que cuesta mucho relacionar con una política—; y matchExpressions de 02-07 resuelve el "uno u otro" en el podSelector.
Hacia la pasarela de pagos
Está fuera del clúster, en pagos.proveedorexterno.example: los selectores no sirven, hay que usar ipBlock.
# k8s/entornos/pro/np-05-pasarela-pagos.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: api-reservas-egress-pagos, namespace: rutas-norte-pro }
spec:
podSelector:
matchLabels: { app: api-reservas }
policyTypes: [Egress]
egress:
- to:
- ipBlock:
cidr: 198.51.100.0/24 # rango publicado por el proveedor de pagos
ports:
- { protocol: TCP, port: 443 }Puntos delicados: un ExternalName no ayuda aquí, porque el Service pasarela-pagos de 04-02 solo crea un CNAME y el tráfico real va a una IP externa que hay que autorizar por ipBlock; las políticas trabajan con IPs, no con nombres, así que si el proveedor cambia de rango la conexión se rompe sin aviso —usa el rango que publica y documenta, no la IP que devuelva un dig hoy—; el DNS ya está permitido por la política del paso 2, sin la cual api-reservas ni podría resolver el nombre; y si no hay rangos estables, la alternativa es un proxy de salida con lista blanca por dominio, o Cilium, con políticas FQDN.
El resultado completo
flowchart LR
NET["Internet"] --> IC["ingress-nginx"]
IC -->|":8080"| TW["tienda-web"]
IC -->|":3000"| API["api-reservas"]
API -->|":5432"| PG["postgres-reservas"]
API -->|":6379"| RC["redis-cache"]
API -->|":443 ipBlock"| PAY["pagos.proveedorexterno.example"]
WK["worker-notificaciones"] -->|":5432"| PG
ALL["Todos los pods"] -->|":53 UDP/TCP"| DNS["CoreDNS (kube-system)"]
X["Cualquier otro pod"] -.->|"DENEGADO"| PG
Todo lo que no aparece en ese diagrama está denegado.
- Verificación sistemática
Una política sin verificar es una suposición: hay que comprobar las dos caras.
#!/bin/bash
# verificar-politicas.sh -- comprobacion de rutas-norte-pro
NS=rutas-norte-pro
probar() { # probar <descripcion> <deploy> <destino> <puerto> <esperado:OK|KO>
RES=$(kubectl exec -n $NS deploy/$2 -- timeout 5 nc -z $3 $4 2>&1 && echo OK || echo KO)
[ "$RES" = "$5" ] && echo " PASA $1" || echo " FALLA $1 (esperado $5)"
}
# Debe FUNCIONAR
probar "api-reservas -> postgres" api-reservas postgres-reservas 5432 OK
probar "api-reservas -> redis" api-reservas redis-cache 6379 OK
probar "worker -> postgres" worker-notificaciones postgres-reservas 5432 OK
# Debe FALLAR
probar "tienda-web -> postgres" tienda-web postgres-reservas 5432 KO
probar "worker -> redis" worker-notificaciones redis-cache 6379 KO
probar "tienda-web -> redis" tienda-web redis-cache 6379 KOY la prueba que da sentido a todo, con el pod intruso del apartado 1:
kubectl run intruso --rm -it --restart=Never -n rutas-norte-pro \
--image=nicolaka/netshoot -- timeout 5 nc -zv postgres-reservas 5432
# nc: connect to postgres-reservas port 5432 (tcp) timed out: Operation now in progressTiempo de espera agotado. El agujero que arrastrábamos desde el módulo 3 está cerrado. Fíjate en el detalle: es tiempo agotado, no "conexión rechazada", porque las NetworkPolicies descartan los paquetes en silencio, sin enviar un RST. Esa firma sirve para diagnosticar:
| Síntoma | Causa probable |
|---|---|
connection refused (inmediato) |
Hay conectividad; el proceso no escucha en ese puerto |
timed out (5-30 s) |
NetworkPolicy descartando, o un problema de red |
EAI_AGAIN / no such host |
DNS bloqueado o caído |
503 en el Ingress |
Endpoints vacíos, o política bloqueando al controlador |
- Limitaciones reales
Son imprescindibles y también claramente limitadas. Hay que saber qué no hacen.
| Limitación | Detalle | Qué lo cubre |
|---|---|---|
| Operan en L3/L4 | Solo IP, puerto y protocolo: no entienden HTTP, rutas, métodos ni cabeceras | Cilium (L7) o una malla |
| Sin identidad criptográfica | La "identidad" es una etiqueta: quien pueda crear pods con ella suplanta al componente | mTLS con una malla (08-04) |
| No registran denegaciones | No hay log de paquetes descartados; se depura a base de tiempos de espera | Hubble (Cilium), flujos de Calico |
| Sin "denegar" explícito | Solo se suma permiso, no caben excepciones restrictivas | Políticas propias de Calico (order, Deny) |
| Sin DNS/FQDN ni cifrado | ipBlock con IPs que cambian sin avisar; restringen quién habla, no protegen el contenido |
FQDN de Cilium, proxy de salida, WireGuard |
No aplican a hostNetwork |
Un pod con red de host escapa al modelo | Pod Security Standards (08-03) |
Lo que aportarían Cilium o una malla en Rutas Norte: permitir solo GET /disponibilidad y POST /reservas en lugar de "el puerto 3000 entero"; identidad basada en certificados, de modo que crear un pod con app: api-reservas no baste para suplantar al componente; visibilidad de flujos permitidos y denegados, que convierte "esto se queda colgado" en "esta política concreta lo descartó"; y políticas por nombre de dominio para la pasarela de pagos.
Nada de eso quita valor a lo construido: las NetworkPolicies son la capa base y la que exige cualquier auditoría. La estrategia completa se estudia en 08-04.
Errores Comunes y Consejos
- Escribir políticas con un CNI que las ignora. Se aplican sin error y no protegen nada. Verifica siempre con una prueba real, no con el nombre del plugin.
- Olvidar el DNS. El primer
deny-allde egress rompe la resolución en todo el namespace y el síntoma (EAI_AGAIN) no apunta a la política: la regla de DNS es siempre la primera que se escribe. Y permitir solo UDP en el 53 falla de forma intermitente: autoriza UDP y TCP. - El guion de más en
from. Convierte un Y en un O y abre namespaces enteros. Reléelo en cada revisión de código. - Autorizar solo una dirección (hacen falta el
egressdel emisor y elingressdel receptor), o usar el puerto del Service en lugar deltargetPort: las políticas ven el puerto del contenedor. - Aplicar
deny-allenkube-system. Puedes dejar sin DNS, sin métricas y sin Ingress a todo el clúster. - Esperar un "denegar" explícito (no existe: para restringir hay que quitar el permiso amplio) o confundir
timed outconconnection refused: el primero huele a política, el segundo a proceso caído. - Consejo: aplica las políticas primero en
rutas-norte-dev, verifica el guion de comprobación y solo entonces promociona apreypro. Undeny-allmal calculado en producción es una caída total. - Consejo: nombra los ficheros con prefijo numérico (
np-00-deny-all,np-01-allow-dns, ...) para que el orden de lectura refleje el orden de construcción. - Consejo: documenta cada política con la conversación de negocio que autoriza. "
worker-notificacioneslee la reserva para enviar el correo" envejece mucho mejor que "permite 5432".
Ejercicios
Ejercicio 1: Verificar que el CNI aplica las políticas
Determina qué CNI está instalado y demuestra con una prueba práctica —no con el nombre del plugin— si las NetworkPolicies se aplican. Si no, recrea el perfil con Calico y repite.
Ejercicio 2: Construir el modelo completo de rutas-norte-pro
Aplica en orden deny-all, la regla de DNS y las políticas de los apartados 9 y 10, documentando tras cada paso qué funciona y qué deja de funcionar. Al terminar, ejecuta el guion de verificación y comprueba que el pod intruso ya no llega a la base de datos.
Ejercicio 3: Detectar el error de Y frente a O en una revisión
Te llega esta política para revisar. Identifica el fallo, explica qué permite de más y corrígela.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: redis-solo-api, namespace: rutas-norte-pro }
spec:
podSelector:
matchLabels: { app: redis-cache }
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels: { app: api-reservas }
- namespaceSelector:
matchLabels: { entorno: pro }
ports:
- { protocol: TCP, port: 6379 }Soluciones
Ejercicio 1
kubectl get pods -n kube-system -o name | grep -Ei "flannel|calico|cilium|weave"
kubectl create ns prueba-np
kubectl run diana --image=nginx -n prueba-np --labels=app=diana
kubectl expose pod diana --port=80 -n prueba-np
kubectl wait --for=condition=ready pod diana -n prueba-np --timeout=60s
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: deny-all-ingress, namespace: prueba-np }
spec: { podSelector: {}, policyTypes: [Ingress] }
EOF
kubectl run p --rm -it --restart=Never -n prueba-np --image=nicolaka/netshoot \
-- curl -s -o /dev/null --max-time 5 http://diana \
&& echo "EL CNI IGNORA LAS POLITICAS" || echo "EL CNI LAS APLICA (correcto)"
kubectl delete ns prueba-npCon Flannel el curl responderá (las ignora); con Calico o Cilium agotará el tiempo. Si hay que reconstruir, minikube delete -p rutas-norte y recrear el perfil con --cni=calico.
Ejercicio 2
| Paso | Funciona tras aplicarlo | Sigue sin funcionar |
|---|---|---|
1. deny-all |
Solo kubectl exec/logs y las sondas |
Todo el tráfico: DNS, base de datos, caché, Ingress |
2. allow-dns-egress |
Resolución de nombres | Todas las conexiones |
| 3. PostgreSQL y Redis | api-reservas y worker alcanzan sus dependencias |
La entrada desde el Ingress |
| 4. Ingress y pagos | La plataforma completa, incluidos los cobros | Todo lo no autorizado |
for f in np-00-deny-all np-01-allow-dns np-02-postgres \
np-03-api-reservas-egress np-04-desde-ingress np-05-pasarela-pagos; do
kubectl apply -f k8s/entornos/pro/$f.yaml
kubectl exec -n rutas-norte-pro deploy/api-reservas -- \
timeout 5 nc -z postgres-reservas 5432 && echo "$f: postgres OK" || echo "$f: postgres KO"
done
bash verificar-politicas.sh
kubectl run intruso --rm -it --restart=Never -n rutas-norte-pro \
--image=nicolaka/netshoot -- timeout 5 nc -zv postgres-reservas 5432
# nc: connect to postgres-reservas port 5432 (tcp) timed outEjercicio 3
El fallo son los dos guiones en from: el podSelector y el namespaceSelector son elementos distintos de la lista, así que se combinan con O. Lo que permite de más es enorme: cualquier pod, de cualquier namespace etiquetado entorno: pro, puede conectar a redis-cache:6379. La intención era "solo api-reservas" y el resultado es "todo el entorno de producción"; un pod comprometido en cualquier namespace de producción podría leer y envenenar la caché de disponibilidad de plazas.
Corrección: unir ambos selectores en un solo elemento (un Y), o simplemente dejar el podSelector, dado que la política ya vive en rutas-norte-pro.
ingress:
- from:
- podSelector:
matchLabels: { app: api-reservas }
namespaceSelector: # SIN guion: interseccion
matchLabels: { entorno: pro }
ports:
- { protocol: TCP, port: 6379 }kubectl exec -n rutas-norte-pro deploy/api-reservas -- timeout 5 nc -z redis-cache 6379 \
&& echo "api-reservas: OK (debe funcionar)"
kubectl exec -n rutas-norte-pro deploy/tienda-web -- timeout 5 nc -z redis-cache 6379 \
|| echo "tienda-web: DENEGADO (correcto)"Conclusión
Has cerrado el agujero: empezaste demostrando con un pod intruso que sin NetworkPolicies la red es plana y que cualquier contenedor del clúster llega a la base de datos con los datos personales de los clientes, y has terminado con esa misma prueba agotando el tiempo de espera. En el camino has fijado lo que más importa. Primero, que Kubernetes define el objeto pero no lo aplica: lo hace el CNI, y un plugin como Flannel acepta tus políticas sin una sola advertencia y no filtra nada, por lo que la única garantía aceptable es una prueba práctica en cada clúster. Segundo, el modelo aditivo: mientras ninguna política seleccione a un pod, todo pasa; en cuanto una lo hace, ese pod queda aislado en esa dirección y solo pasa lo explícitamente permitido; las políticas se suman, no hay prioridades y no existe denegar, lo que significa que no caben excepciones restrictivas. Tercero, que hacen falta las dos direcciones: el egress del emisor y el ingress del receptor. Y dominas los tres selectores —podSelector para el mismo namespace, namespaceSelector para cruzarlos e ipBlock con except para lo externo— y sobre todo la diferencia entre Y y O, ese guion de más que convierte "solo api-reservas" en "todo el entorno de producción" sin que el clúster diga nada.
Has construido el modelo completo de rutas-norte-pro en el orden correcto: deny-all primero, aunque rompa la plataforma; DNS después, con UDP y TCP hacia CoreDNS, evitando el fallo clásico cuyo síntoma (EAI_AGAIN) apunta a todas partes menos a la política que acabas de aplicar; y luego cada conversación de negocio autorizada una a una: api-reservas y worker-notificaciones hacia PostgreSQL, api-reservas hacia Redis, el controlador de Ingress hacia la tienda y la API por sus targetPort, y api-reservas hacia la pasarela de pagos por ipBlock. Todo verificado por ambas caras, sabiendo que un timed out huele a política y un connection refused a proceso caído. Y conoces los límites: L3/L4, sin identidad criptográfica, sin registro de denegaciones y sin nombres de dominio, ahí donde entran Cilium o una malla de servicio (08-04).
Con esto termina el módulo 4 y Rutas Norte es por primera vez una plataforma real y accesible. Los clientes entran por https://www.rutasnorte.example, las agencias consumen https://api.rutasnorte.example, los certificados se renuevan solos, el descubrimiento entre componentes va por DNS y la red está segmentada de modo que solo las conversaciones legítimas son posibles.
Queda una pieza que arrastramos desde el módulo 2 con una nota de "PROVISIONAL" en el manifiesto: postgres-reservas no tiene almacenamiento persistente. Su Deployment guarda los datos dentro del contenedor, así que cada reinicio del pod —un rollout, un desalojo, la caída de un nodo— borra todas las reservas y todos los clientes. El módulo 5, Almacenamiento en Kubernetes, resuelve exactamente eso: volúmenes, PersistentVolumes y PersistentVolumeClaims, clases de almacenamiento, aprovisionamiento dinámico con expansión y snapshots, y copias de seguridad y restauración. Es el módulo que convierte a Rutas Norte en una plataforma en la que se puede confiar.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
