Saltar al contenido principal

Network Policies avanzadas: microsegmentación

En el capítulo de seguridad vimos los fundamentos de las NetworkPolicies. Ahora toca pensar como un atacante: si compromete un pod, ¿hasta dónde puede llegar moviéndose lateralmente por la red? En un cluster sin políticas, la respuesta es "a todas partes". La microsegmentación consiste en que la respuesta sea "a ningún sitio que no necesite".

Repaso exprés de la semántica

Tres reglas mentales que evitan el 90% de los errores:

  1. Las políticas seleccionan pods (por labels) y aplican al tráfico del tipo declarado en policyTypes (Ingress, Egress o ambos).
  2. Son aditivas y de lista blanca: no hay reglas de denegación; todo lo no permitido por ninguna política queda denegado (una vez el pod está seleccionado por alguna).
  3. Sin política que lo seleccione, un pod lo permite todo. Por eso el primer paso siempre es el default deny.

Y el recordatorio operativo: las aplica la CNI. Cilium y Calico sí; Flannel a secas las ignora silenciosamente.

El punto de partida: default deny total

El patrón base de microsegmentación es denegar todo en cada namespace y abrir solo lo necesario:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: produccion
spec:
podSelector: {} # Todos los pods del namespace
policyTypes:
- Ingress
- Egress

Ojo: esto también rompe el DNS, y sin DNS nada funciona. La primera excepción siempre es permitir la salida hacia CoreDNS:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: produccion
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53

Fíjate en el label kubernetes.io/metadata.name: Kubernetes lo pone automáticamente en todos los namespaces con su nombre, y es la forma estándar de seleccionar un namespace concreto.

La trampa del AND y el OR

La sutileza que más puntos cuesta en el examen CKS. Estos dos bloques no son equivalentes:

# OR: desde pods de namespaces "backend", O desde pods con role=api de MI namespace
ingress:
- from:
- namespaceSelector:
matchLabels:
tier: backend
- podSelector:
matchLabels:
role: api
# AND: desde pods con role=api QUE ESTÉN en namespaces "backend"
ingress:
- from:
- namespaceSelector:
matchLabels:
tier: backend
podSelector:
matchLabels:
role: api

La diferencia es un guion: dos elementos de lista (OR) frente a un único elemento con dos selectores (AND). Revísalo siempre dos veces.

Del mismo modo, varias entradas en ingress: o egress: se combinan con OR, y cada entrada puede restringir además los ports.

Patrón completo: la aplicación de tres capas

El ejercicio canónico: frontend → backend → base de datos, cada uno hablando solo con su vecino.

# La base de datos solo acepta tráfico del backend, y solo al 5432
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-policy
namespace: produccion
spec:
podSelector:
matchLabels:
app: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 5432
---
# El backend: entra el frontend, sale solo hacia la BD
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-policy
namespace: produccion
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: db
ports:
- protocol: TCP
port: 5432

Con el default-deny y allow-dns anteriores, un atacante que comprometa el frontend solo puede alcanzar el backend por el 8080. Ni etcd, ni el metadata service de la nube, ni otros namespaces. Eso es microsegmentación.

Aislar namespaces enteros

Otro patrón muy pedido: que un namespace solo acepte tráfico de sí mismo:

spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- podSelector: {} # Cualquier pod... del MISMO namespace

Un podSelector vacío dentro de from se evalúa en el namespace de la política. Para permitir además un namespace concreto (por ejemplo, el de monitorización), se añade otro elemento con namespaceSelector.

Bloquear el exterior y la red del nodo

Para tráfico de salida hacia fuera del cluster se usa ipBlock. Patrón típico de defensa: permitir salir a Internet pero nunca a la red interna ni al metadata service del cloud (169.254.169.254 es el objetivo número uno tras comprometer un pod):

egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 169.254.169.254/32

Más allá del estándar: políticas de la CNI

Las NetworkPolicies estándar son deliberadamente limitadas: capa 3/4, sin reglas de denegación explícita, sin políticas de cluster. Las CNIs serias las extienden con CRDs propias:

  • CiliumNetworkPolicy / CiliumClusterwideNetworkPolicy: añaden filtrado L7 (HTTP por método y ruta, llamadas gRPC concretas), políticas por nombre DNS (toFQDNs: ["api.github.com"]) y alcance de cluster.
  • Calico NetworkPolicy / GlobalNetworkPolicy: orden y prioridad de políticas, reglas Deny explícitas, y aplicación también a los hosts.

Un ejemplo de la potencia L7 de Cilium, imposible con el estándar:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: api-l7
spec:
endpointSelector:
matchLabels:
app: backend
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "8080"
protocol: TCP
rules:
http:
- method: GET
path: "/api/v1/.*"

Para el examen CKS basta el estándar, pero en el mundo real estas extensiones son las que permiten un zero trust de verdad (junto al mTLS del service mesh, que asegura el cómo mientras las políticas deciden el quién).

Probar y depurar políticas

# Ver qué políticas hay y a quién seleccionan
kubectl get netpol -A
kubectl describe netpol db-policy -n produccion

# Probar conectividad desde un pod con los labels adecuados
kubectl run test --rm -it --image=busybox:1.36 --labels=app=backend -- \
wget -qO- --timeout=2 http://db.produccion:5432

El error más común al depurar: probar desde un pod sin los labels que la política espera. El segundo más común: olvidar el DNS tras un default deny de egress.

Resumen

  • Microsegmentación = default deny (Ingress y Egress) por namespace + excepciones mínimas, empezando por el DNS.
  • Cuidado con el guion: lista de selectores = OR; selectores juntos en un elemento = AND.
  • Patrones clave: tres capas, aislamiento de namespace, egress con ipBlock y except (¡el metadata service!).
  • Las CRDs de Cilium/Calico extienden el estándar con L7, FQDNs y deny explícito.

Volver al índice