Hemos cerrado dos dimensiones de la seguridad de Rutas Norte. Con RBAC (08-01) controlamos quién puede hacer qué contra la API. Con los contextos de seguridad (08-02) y las políticas de admisión (08-03) controlamos qué puede hacer un contenedor una vez está corriendo, y conseguimos que el clúster lo haga cumplir por sí solo.
Queda una dimensión entera: qué puede hablar con qué. Y aquí ya hicimos trabajo importante en 04-06, cuando pusimos una NetworkPolicy deny-all en rutas-norte-pro y autorizamos cada conversación una a una. Pero entonces señalamos tres límites incómodos que dejamos aparcados:
- Las NetworkPolicies trabajan en L3/L4: entienden de IP y de puertos, no de rutas HTTP ni de métodos.
api-reservaspuede llegar apostgres-reservas, sí, pero la política no distingue entre una consulta legítima y un volcado completo de la tabla de clientes. - No registran nada. Un intento de conexión denegado es completamente invisible. Si alguien está sondeando la red interna, no nos enteramos.
- Dentro del clúster, todo el tráfico viaja sin cifrar una vez pasado el TLS del Ingress (04-05). Quien pueda observar la red del nodo ve las consultas a
postgres-reservasen claro, con nombres, DNI y teléfonos incluidos.
Y hay una cuarta cosa de la que apenas hemos hablado: el tráfico saliente. Es la vía natural por la que sale una base de datos de clientes.
Esta lección no repite la sintaxis de NetworkPolicy: la das por sabida y la usamos. Lo que construimos aquí es la estrategia de red completa de una plataforma en producción, por capas.
Advertencia importante. El diseño de la arquitectura de red de una plataforma de producción debe revisarlo un profesional de seguridad, que evaluará el modelo de amenazas concreto. Y como el tráfico de Rutas Norte transporta datos personales de clientes —nombre, DNI, teléfono y correo—, las decisiones sobre cifrado en tránsito, control de salida y registro de flujos deben conocerlas y aprobarlas también el responsable de cumplimiento normativo. El enfoque de esta lección es exclusivamente defensivo: entender los caminos por los que puede fugarse información para cerrarlos y detectarlos.
Contenido
- Las capas de defensa en red
- Microsegmentación como principio
- Control del tráfico saliente
- El problema de las IP frente a los nombres de dominio
- Cifrado en tránsito dentro del clúster
- Qué es una malla de servicio y qué cuesta
- Istio, Linkerd y Cilium comparados
- Políticas de autorización de nivel 7
- Protección del perímetro
- Seguridad del plano de control
- Registro y visibilidad del tráfico
- Confianza cero aplicada a Rutas Norte
- Errores comunes y consejos
- Ejercicios
- Conclusión
- Las capas de defensa en red
La seguridad de red no es un mecanismo: es una serie de capas, cada una de las cuales asume que la anterior puede fallar. Este es el modelo completo que vamos a construir.
flowchart TB
I["Internet"] --> WAF["Capa 1: perímetro<br/>WAF + limitación de peticiones<br/>+ protección DoS"]
WAF --> ING["Capa 2: Ingress<br/>TLS terminado (04-05)<br/>cabeceras de seguridad"]
ING --> NP["Capa 3: microsegmentación<br/>NetworkPolicy deny-all + permisos<br/>explícitos (04-06)"]
NP --> MESH["Capa 4: mTLS + autorización L7<br/>identidad por carga de trabajo<br/>(malla de servicio)"]
MESH --> APP["Cargas de Rutas Norte"]
APP --> EGR["Capa 5: control de salida<br/>solo la pasarela de pagos"]
EGR --> EXT["pagos.proveedorexterno.example"]
APP -.-> OBS["Capa 6: visibilidad<br/>registro de flujos<br/>(Hubble)"]
CP["Capa 7: plano de control<br/>apiserver, etcd, kubelet"] -.-> APP
style NP fill:#d5e8f9,stroke:#36c
style EGR fill:#f9d5d5,stroke:#c33
| Capa | Qué mitiga | Estado en Rutas Norte |
|---|---|---|
| 1. Perímetro | Ataques desde internet, denegación de servicio, abuso | Por construir |
| 2. Ingress con TLS | Escucha del tráfico entre cliente y plataforma | Hecho (04-05) |
| 3. Microsegmentación | Movimiento lateral tras comprometer un pod | Hecho (04-06), a reforzar |
| 4. mTLS y autorización L7 | Escucha interna, suplantación entre servicios | Por decidir |
| 5. Control de salida | Exfiltración de datos de clientes | Por construir |
| 6. Visibilidad | Ceguera ante un incidente | Por construir |
| 7. Plano de control | Compromiso total del clúster | Por revisar |
El orden importa: cada capa se justifica por lo que ocurriría si la anterior fallase. Si el WAF no detiene un ataque, el Ingress todavía valida el certificado. Si alguien consigue ejecutar código en tienda-web, la microsegmentación impide que llegue a postgres-reservas. Si consiguiera llegar, el mTLS impediría que se hiciera pasar por api-reservas. Y si todo lo anterior fallase, el control de salida impediría que los datos salieran del clúster.
- Microsegmentación como principio
La microsegmentación consiste en tratar cada carga de trabajo como su propio segmento de red, con reglas explícitas de qué puede hablar con qué. Es lo contrario del modelo tradicional de "red interna de confianza detrás de un cortafuegos".
Por qué el modelo del perímetro no sirve en Kubernetes
En una red clásica, el cortafuegos separaba "dentro" de "fuera", y dentro todo el mundo se hablaba. En un clúster de Kubernetes, por defecto, todos los pods pueden hablar con todos los pods de todos los namespaces. Un curl desde tienda-web llega a postgres-reservas sin obstáculo.
Recuerda lo que dijimos en 02-06: un namespace no es una frontera de seguridad por sí solo. Sin NetworkPolicies, rutas-norte-dev y rutas-norte-pro están en la misma red plana.
La consecuencia práctica: si alguien consigue ejecutar código en tienda-web —el componente más expuesto, porque atiende directamente a internet— tiene acceso de red a toda la plataforma. La microsegmentación convierte eso en "tiene acceso de red a api-reservas, y solo al puerto 8080".
Los tres principios
Principio 1: denegar por defecto en los tres entornos.
En 04-06 aplicamos deny-all en rutas-norte-pro. Eso no basta. rutas-norte-dev y rutas-norte-pre también lo necesitan, por dos razones:
- Si desarrollo y preproducción están abiertos, una política que funciona ahí puede fallar en producción, y el fallo se descubre en el peor momento.
- Un pod comprometido en
rutas-norte-devsin restricciones de red puede alcanzarrutas-norte-pro, porque la red del clúster es plana.
En 08-03 resolvimos esto elegantemente con una política de Kyverno que genera la deny-all en todo namespace nuevo de Rutas Norte, con synchronize: true para que vuelva si alguien la borra. Es la garantía de que ningún namespace futuro nazca abierto.
Principio 2: políticas por componente, no por namespace.
Es tentador escribir "todo lo de rutas-norte-pro puede hablar con todo lo de rutas-norte-pro". Es un error: reproduce dentro del namespace el mismo modelo plano que queríamos evitar.
| Enfoque | Qué permite si se compromete tienda-web |
|---|---|
| Por namespace | Acceso a postgres-reservas, redis-cache, api-reservas y todo lo demás |
| Por componente | Solo api-reservas:8080. Nada más |
La diferencia es enorme y el coste es escribir seis políticas en lugar de una.
Principio 3: revisión periódica de qué conversaciones siguen siendo necesarias.
Las políticas de red se degradan igual que el RBAC. Se añade un permiso para depurar y se queda. Se retira un componente y su política sobrevive. Se cambia un puerto y se deja el viejo "por si acaso".
La matriz de comunicaciones autorizadas de Rutas Norte, tal como quedó en 04-06, debe caber en una tabla y revisarse cada trimestre:
| Origen | Destino | Puerto | Justificación | Última revisión |
|---|---|---|---|---|
| Ingress | tienda-web |
8080 | Tráfico público | 2026-07 |
| Ingress | api-reservas |
8080 | API pública | 2026-07 |
tienda-web |
api-reservas |
8080 | Consultas y reservas | 2026-07 |
api-reservas |
postgres-reservas |
5432 | Datos de reservas y clientes | 2026-07 |
api-reservas |
redis-cache |
6379 | Caché de disponibilidad | 2026-07 |
api-reservas |
Internet (pasarela) | 443 | Cobros | 2026-07 |
worker-notificaciones |
postgres-reservas |
5432 | Lee reservas pendientes | 2026-07 |
worker-notificaciones |
Internet (SMTP) | 587 | Envío de correos | 2026-07 |
informes-ocupacion |
postgres-reservas |
5432 | Agregados nocturnos | 2026-07 |
| Prometheus | Todos | 9090 | Métricas (07-03) | 2026-07 |
| Todos | CoreDNS | 53 | Resolución de nombres | 2026-07 |
Cada fila debe tener un porqué vivo. Una pregunta útil en la revisión: si borro esta regla ahora mismo, ¿qué se rompe? Si nadie lo sabe, hay que averiguarlo (en preproducción) o quitarla.
Un guion para detectar políticas huérfanas:
#!/usr/bin/env bash
# k8s/seguridad/politicas-huerfanas.sh
# Detecta NetworkPolicies cuyo podSelector no encaja con ningún pod.
set -euo pipefail
NS="${1:-rutas-norte-pro}"
kubectl get networkpolicy -n "$NS" -o json | jq -r '
.items[] | select(.spec.podSelector.matchLabels != null)
| "\(.metadata.name)\t" +
([.spec.podSelector.matchLabels | to_entries[] | "\(.key)=\(.value)"] | join(","))' \
| while IFS=$'\t' read -r politica selector; do
n=$(kubectl get pods -n "$NS" -l "$selector" --no-headers 2>/dev/null | wc -l)
[[ "$n" -eq 0 ]] && echo "HUÉRFANA: $politica (selector: $selector) no encaja con ningún pod"
doneEsa política sobrevivió al componente que protegía. No es peligrosa por sí misma, pero es ruido que dificulta la revisión, y si mañana alguien despliega algo con esa etiqueta hereda permisos que nadie decidió.
- Control del tráfico saliente
Aquí está, probablemente, el hueco más grave que queda en Rutas Norte.
Por qué el egreso importa tanto
Piensa en la secuencia de un incidente:
- Alguien encuentra un fallo en
api-reservasy consigue ejecutar código. - El proceso tiene acceso legítimo a
postgres-reservas: puede leer la tabla de clientes con nombres, DNI, teléfonos y correos. - Ahora necesita sacar esos datos del clúster.
El paso 3 es el que convierte un compromiso en una fuga de datos. Sin control de salida, el paso 3 es trivial: una petición HTTPS a cualquier servidor de internet.
Regla que hay que interiorizar: la política de entrada limita quién entra; la política de salida limita qué puede salir. Casi todo el mundo escribe la primera y olvida la segunda. Y la segunda es la que decide si un incidente se queda en "alguien ejecutó código" o escala a "se filtraron los datos personales de 200.000 clientes".
La mayoría de las guías de NetworkPolicy solo hablan de Ingress. Comprueba tus políticas: si policyTypes no incluye Egress, el tráfico de salida está completamente abierto.
La salida que Rutas Norte necesita
Auditemos qué salidas son legítimas:
| Componente | Destino externo | Puerto | ¿Imprescindible? |
|---|---|---|---|
api-reservas |
pagos.proveedorexterno.example |
443 | Sí: cobros |
worker-notificaciones |
Servidor SMTP del proveedor | 587 | Sí: correos de confirmación |
tienda-web |
Ninguno | — | No necesita salir a internet |
postgres-reservas |
Ninguno | — | No |
redis-cache |
Ninguno | — | No |
informes-ocupacion |
Ninguno | — | No |
Cuatro de los seis componentes no necesitan salir a internet en absoluto. Eso es lo primero que hay que aprovechar: cerrarles la salida por completo es gratis y elimina la vía de exfiltración para la mayor parte de la plataforma.
La política de salida base
Recordando que en 04-06 dejamos deny-all con policyTypes: [Ingress, Egress], cada componente necesita explícitamente su salida. Lo mínimo universal es DNS:
# k8s/base/red/permitir-dns.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: permitir-dns-salida
namespace: rutas-norte-pro
spec:
podSelector: {} # todos los pods
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: 53Sin esto, ningún pod resuelve nombres y todo falla de una forma muy confusa: los servicios no se encuentran entre sí aunque las políticas de ingreso estén bien. Es el error más frecuente al activar Egress por primera vez.
La salida a la pasarela de pagos
api-reservas necesita llegar a pagos.proveedorexterno.example. Aquí es donde aparece el problema del apartado siguiente, pero veamos primero la solución con ipBlock:
# k8s/base/red/api-reservas-salida.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-reservas-salida
namespace: rutas-norte-pro
annotations:
seguridad.rutasnorte.example/justificacion: >-
Salida a la pasarela de pagos externa. Los rangos de IP los publica el
proveedor en su documentación y se revisan mensualmente mediante el
trabajo programado "verificar-rangos-pasarela". Última verificación
documentada en el registro de cambios de red.
spec:
podSelector:
matchLabels:
app: api-reservas
policyTypes: [Egress]
egress:
# 1. DNS (imprescindible)
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
# 2. Base de datos y caché (tráfico interno)
- to:
- podSelector:
matchLabels:
app: postgres-reservas
ports:
- { protocol: TCP, port: 5432 }
- to:
- podSelector:
matchLabels:
app: redis-cache
ports:
- { protocol: TCP, port: 6379 }
# 3. Pasarela de pagos: SOLO estos rangos, SOLO el 443
- to:
- ipBlock:
cidr: 203.0.113.0/24 # rango publicado por el proveedor
- ipBlock:
cidr: 198.51.100.64/26 # rango secundario
ports:
- { protocol: TCP, port: 443 }Lo importante de este manifiesto no es lo que permite, sino lo que no permite: api-reservas no puede conectar con ninguna otra dirección de internet. Si alguien consigue ejecutar código ahí y quiere enviar la tabla de clientes a un servidor propio, la conexión no sale.
Nota sobre ipBlock y las IP privadas: ipBlock se refiere a IP de red, y los pods del clúster también tienen IP. Un ipBlock: 0.0.0.0/0 incluiría todo el tráfico interno del clúster. Cuando se quiere "todo internet menos la red interna", hay que usar except:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8 # red de pods y servicios
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- { protocol: TCP, port: 443 }Esto es "cualquier destino público en el 443", que es mucho más laxo que la lista de rangos del proveedor y no lo recomendamos para api-reservas. Lo incluimos porque es un patrón que aparece a menudo y conviene entender por qué es peor: permite enviar datos a cualquier servidor de internet que hable HTTPS.
El contenedor embajador
Recuerda de 06-04 que la conexión con la pasarela de pagos pasa por un contenedor embajador dentro del pod de api-reservas. Eso tiene una consecuencia de red importante: como los contenedores de un pod comparten el namespace de red, la política se aplica al pod entero, no al contenedor. El embajador y el contenedor principal tienen exactamente los mismos permisos de red.
El embajador aporta otras ventajas —centraliza los reintentos, la lógica de tiempo de espera y las credenciales de la pasarela— pero no es un límite de seguridad de red. Si quisiéramos que solo el embajador pudiera hablar con la pasarela, tendría que ser un pod aparte con su propia política.
Es un buen ejemplo de una confusión común: el patrón embajador es un patrón de arquitectura, no de aislamiento.
- El problema de las IP frente a los nombres de dominio
Aquí llega la limitación estructural de las NetworkPolicies.
Las NetworkPolicies estándar de Kubernetes trabajan con direcciones IP y rangos CIDR. No entienden de nombres de dominio. No existe un campo
to: dnsName: pagos.proveedorexterno.example.
Y esto no es un descuido: las políticas se traducen a reglas de cortafuegos en el kernel del nodo, que actúa sobre paquetes IP. Cuando el paquete llega ahí, el nombre ya se resolvió y no queda rastro de él.
Los tres problemas prácticos
Problema 1: las IP cambian. Los servicios en la nube rotan direcciones. La documentación del proveedor de pagos puede publicar rangos hoy y cambiarlos en tres meses. Si tu ipBlock se queda desfasado, los pagos dejan de funcionar y el diagnóstico es especialmente ingrato porque nada en los logs dice "una NetworkPolicy ha bloqueado esto".
Problema 2: un rango es más de lo que quieres. 203.0.113.0/24 son 256 direcciones. Si el proveedor comparte infraestructura, ese rango puede incluir servidores de otros clientes suyos. Has autorizado la salida a 256 destinos para llegar a uno.
Problema 3: el escenario inverso es peor. Si un servicio legítimo comparte IP con un servicio de almacenamiento genérico (algo habitual en las nubes públicas), autorizar el primero autoriza el segundo, y ahí sí hay una vía de exfiltración.
Las tres opciones
| Opción | Cómo funciona | Ventajas | Inconvenientes |
|---|---|---|---|
| Pasarela de salida (egress gateway) | Todo el tráfico saliente pasa por unos pods concretos con IP fija, que aplican las reglas | Punto único de control y registro; IP de origen estable para que el proveedor pueda filtrarla | Otra pieza que mantener; punto único de fallo |
| Proxy con lista de permitidos | Un proxy HTTP(S) al que apuntan las aplicaciones, con una lista de dominios autorizados | Filtra por nombre de dominio, incluido SNI; registro completo de destinos | Las aplicaciones deben configurarse para usarlo; añade latencia |
| Políticas basadas en DNS (Cilium) | El CNI observa las respuestas DNS y programa las reglas con las IP que devuelven | Se escribe el nombre directamente; se actualiza solo | Requiere Cilium como CNI |
Opción A: proxy de salida con lista de permitidos
Es la opción más portátil y la que da mejor visibilidad. Un Squid o un proxy dedicado en un pod, con una lista de dominios:
# k8s/base/red/proxy-salida-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: proxy-salida-config
namespace: rutas-norte-sistema
data:
dominios-permitidos.txt: |
# Pasarela de pagos: cobros de billetes
pagos.proveedorexterno.example
# Correo saliente: confirmaciones de reserva
smtp.proveedorcorreo.example
# NADA MÁS. Cualquier otro destino se rechaza y se registra.Las aplicaciones se configuran con HTTPS_PROXY, y la NetworkPolicy solo permite salir hacia el proxy:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-reservas-salida-por-proxy
namespace: rutas-norte-pro
spec:
podSelector:
matchLabels:
app: api-reservas
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- { protocol: UDP, port: 53 }
# Única salida permitida: el proxy. Ni una IP de internet directamente.
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: rutas-norte-sistema
podSelector:
matchLabels:
app: proxy-salida
ports:
- { protocol: TCP, port: 3128 }Ventaja decisiva: el proxy registra cada destino solicitado, permitido o denegado. Eso resuelve parcialmente el problema de la falta de registro de las NetworkPolicies, al menos para el tráfico saliente, que es el que más importa vigilar.
Inconveniente honesto: una aplicación que ignore las variables HTTPS_PROXY se salta el proxy. Por eso la NetworkPolicy sigue siendo necesaria: impide la salida directa, obligando a pasar por el proxy aunque la aplicación no coopere. Las dos capas juntas funcionan; ninguna por separado.
Opción B: políticas basadas en DNS con Cilium
Si el CNI es Cilium, la CiliumNetworkPolicy permite escribir el nombre directamente:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: api-reservas-salida-dns
namespace: rutas-norte-pro
spec:
endpointSelector:
matchLabels:
app: api-reservas
egress:
# DNS con inspección: Cilium observa las respuestas para programar las reglas
- toEndpoints:
- matchLabels:
io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports:
- { port: "53", protocol: UDP }
rules:
dns:
- matchPattern: "*"
# Salida por NOMBRE DE DOMINIO, no por IP
- toFQDNs:
- matchName: "pagos.proveedorexterno.example"
toPorts:
- ports:
- { port: "443", protocol: TCP }Cómo funciona: Cilium intercepta las respuestas DNS del pod, ve qué IP devolvió pagos.proveedorexterno.example y programa dinámicamente la regla para esa IP, con el tiempo de vida que indique el registro DNS. Si mañana el proveedor cambia de IP, la política sigue funcionando sin que nadie toque nada.
Es, con diferencia, la solución más elegante. Su coste es la dependencia del CNI: adoptar Cilium es una decisión de plataforma que va mucho más allá de esta política.
Recomendación para Rutas Norte
Un enfoque en dos tiempos:
- Ahora:
ipBlockcon los rangos publicados por el proveedor, más un trabajo programado mensual que verifique que siguen siendo válidos y avise si cambian. Es la solución que funciona con cualquier CNI y no requiere nuevas piezas. - A medio plazo: proxy de salida con lista de permitidos, principalmente por el registro de destinos. Si en algún momento se migra a Cilium (cosa que también aportaría las ventajas del apartado 11), se sustituyen las políticas por
toFQDNs.
Y la comprobación que hay que automatizar en cualquier caso:
#!/usr/bin/env bash
# k8s/seguridad/verificar-salida.sh
# Confirma que los componentes que no deben salir a internet, no salen.
set -uo pipefail
NS=rutas-norte-pro
comprobar_bloqueo() {
local carga="$1" destino="$2"
local salida
salida=$(kubectl exec -n "$NS" "$carga" -- \
timeout 5 wget -q -O- --timeout=4 "$destino" 2>&1 || echo "BLOQUEADO")
if [[ "$salida" == *"BLOQUEADO"* ]]; then
echo "OK $carga no alcanza $destino"
else
echo "FALLO $carga SÍ alcanza $destino <-- vía de exfiltración abierta"
fi
}
comprobar_bloqueo deploy/tienda-web https://ejemplo-externo.example
comprobar_bloqueo statefulset/postgres-reservas https://ejemplo-externo.example
comprobar_bloqueo deploy/api-reservas https://ejemplo-externo.exampleOK deploy/tienda-web no alcanza https://ejemplo-externo.example
OK statefulset/postgres-reservas no alcanza https://ejemplo-externo.example
OK deploy/api-reservas no alcanza https://ejemplo-externo.exampleEsta prueba debe ejecutarse en rutas-norte-pre en cada despliegue. Es la verificación de la capa que impide una fuga de datos personales, y como tal debería estar en el expediente que revisa el responsable de cumplimiento.
- Cifrado en tránsito dentro del clúster
En 04-05 pusimos TLS en el Ingress con cert-manager: el tráfico entre el navegador del cliente y la plataforma va cifrado hasta https://www.rutasnorte.example. Perfecto. Pero ¿qué pasa después del Ingress?
Qué queda sin cifrar
flowchart LR
C["Navegador"] -->|"HTTPS<br/>cifrado (04-05)"| ING["Ingress"]
ING -->|"HTTP<br/>EN CLARO"| TW["tienda-web"]
TW -->|"HTTP<br/>EN CLARO"| API["api-reservas"]
API -->|"protocolo PostgreSQL<br/>EN CLARO"| PG[("postgres-reservas<br/>datos personales")]
API -->|"RESP<br/>EN CLARO"| R[("redis-cache")]
style PG fill:#f9d5d5,stroke:#c33
Todo lo que hay a la derecha del Ingress viaja sin cifrar por la red del clúster. Y por ese tramo pasan, en claro:
- Las consultas SQL con nombres, DNI, teléfonos y correos de clientes.
- Las respuestas de la API con los datos de las reservas.
- Las credenciales de la base de datos, en el saludo inicial de cada conexión.
¿Quién puede ver ese tráfico?
Esta es la pregunta que decide si merece la pena el esfuerzo:
| Escenario | ¿Ve el tráfico interno? |
|---|---|
| Un pod normal con NetworkPolicies estrictas | No (no puede ni conectar) |
Un pod con hostNetwork: true |
Sí: ve todo el tráfico del nodo |
Un pod con CAP_NET_RAW y hostNetwork |
Sí: puede capturar paquetes |
| Alguien con acceso al nodo (SSH, contenedor privilegiado) | Sí, todo |
| El proveedor de la nube o del centro de datos | Depende del contrato y la infraestructura |
| Un atacante que compromete el CNI | Sí |
Fíjate en cómo se conectan las lecciones: en 08-02 prohibimos hostNetwork y CAP_NET_RAW, y en 08-03 hicimos que el clúster lo rechace. Ese trabajo es lo que hace que el tráfico interno sin cifrar sea un riesgo aceptable en muchos escenarios. El cifrado interno es la defensa para cuando esa capa falla, o cuando la normativa lo exige explícitamente.
Las tres opciones de cifrado interno
| Opción | Qué cifra | Coste | Identidad |
|---|---|---|---|
| TLS en la aplicación | Solo lo que la aplicación implemente | Cambios de código en cada componente | Certificados gestionados a mano |
| Cifrado del CNI (WireGuard, IPsec) | Todo el tráfico entre nodos | Bajo: una opción de configuración | Por nodo, no por carga |
| mTLS con malla de servicio | Todo el tráfico entre pods de la malla | Alto: una capa de infraestructura completa | Por carga de trabajo |
Opción 1: TLS a nivel de aplicación
Es lo que ya hacemos con la pasarela de pagos (el embajador habla HTTPS). Para el tráfico interno significaría configurar PostgreSQL con ssl = on, generar certificados, distribuirlos y configurar cada cliente.
# Fragmento: PostgreSQL con TLS obligatorio
env:
- name: POSTGRES_INITDB_ARGS
value: "--auth-host=scram-sha-256"
volumeMounts:
- name: certificados-tls
mountPath: /var/lib/postgresql/certs
readOnly: trueVentaja: cifra el tramo que más importa (los datos personales) con poco despliegue. Inconveniente: hay que hacerlo componente a componente, gestionar la rotación de certificados y confiar en que cada cliente verifique realmente el certificado (muchos clientes de base de datos, por defecto, no lo hacen).
Para Rutas Norte, cifrar específicamente la conexión con postgres-reservas es una medida de alto valor y coste moderado, y probablemente el primer paso a dar.
Opción 2: cifrado a nivel de CNI
Varios CNI pueden cifrar automáticamente todo el tráfico entre nodos con WireGuard o IPsec.
# Cilium con cifrado WireGuard (valores de Helm)
encryption:
enabled: true
type: wireguard
nodeEncryption: true# Calico con WireGuard
kubectl patch felixconfiguration default --type=merge \
-p '{"spec":{"wireguardEnabled":true}}'| A favor | En contra | |
|---|---|---|
| Esfuerzo | Mínimo: una opción | — |
| Cobertura | Todo el tráfico entre nodos, sin excepciones | Solo entre nodos: dos pods en el mismo nodo no se cifran |
| Rendimiento | WireGuard es muy eficiente | Algo de CPU; con IPsec, más |
| Identidad | — | Autentica nodos, no cargas de trabajo |
Ese último punto es la limitación esencial: el cifrado del CNI protege contra quien observe la red entre nodos, pero no aporta identidad por servicio. tienda-web y api-reservas en el mismo nodo se hablan igual de en claro, y ninguno puede demostrar criptográficamente quién es.
Aun así, la relación valor/esfuerzo es excelente. Si el CNI lo soporta, actívalo.
Opción 3: mTLS con una malla de servicio
Es el siguiente apartado, porque da mucho más que cifrado.
- Qué es una malla de servicio y qué cuesta
Una malla de servicio (service mesh) es una capa de infraestructura que se ocupa de la comunicación entre servicios, sacándola del código de las aplicaciones.
Cómo funciona
En el modelo clásico, se inyecta un proxy sidecar (habitualmente Envoy) en cada pod. Todo el tráfico que entra y sale del pod pasa por ese proxy, que es quien aplica el cifrado, la autorización y la recogida de métricas. La aplicación no se entera: sigue haciendo http://api-reservas:8080.
flowchart LR
subgraph P1["Pod tienda-web"]
A1["nginx"] <--> S1["proxy sidecar"]
end
subgraph P2["Pod api-reservas"]
S2["proxy sidecar"] <--> A2["Node.js"]
end
S1 <-->|"mTLS<br/>cifrado + identidad<br/>+ autorización L7"| S2
CP["Plano de control<br/>emite certificados<br/>distribuye políticas"] -.-> S1
CP -.-> S2
Qué te da
| Capacidad | Qué resuelve | ¿Se puede tener sin malla? |
|---|---|---|
| Identidad criptográfica por carga | Cada servicio tiene un certificado que demuestra quién es | Muy difícil a mano |
| mTLS automático | Cifrado y autenticación mutua sin tocar código | Difícil, componente a componente |
| Autorización L7 | "Solo tienda-web puede hacer POST /reservas" |
No con NetworkPolicy |
| Reintentos y tiempos de espera | Resiliencia sin código | Sí, en cada aplicación |
| Cortacircuitos | Aislar un servicio degradado | Sí, con bibliotecas |
| Despliegues canary por porcentaje | Enviar el 5 % del tráfico a la versión nueva | Parcialmente (11-04) |
| Observabilidad de la comunicación | Métricas de latencia y errores de cada llamada | Parcialmente (07-03) |
La identidad por carga de trabajo es la joya de la corona y merece que la entendamos bien. Sin malla, cuando postgres-reservas recibe una conexión desde una IP, lo único que sabe es que viene de esa IP. Con la NetworkPolicy sabemos que esa IP corresponde a un pod con la etiqueta app: api-reservas... pero las etiquetas las pone quien crea el pod, y una IP se puede suplantar.
Con mTLS, postgres-reservas recibe un certificado que dice spiffe://cluster.local/ns/rutas-norte-pro/sa/api-reservas, firmado por la autoridad certificadora del clúster, que nadie más puede falsificar. Eso sí es identidad. Y encaja perfectamente con las ServiceAccounts de 03-06.
Qué te cuesta
Esta es la parte que las presentaciones comerciales pasan por encima:
| Coste | Detalle |
|---|---|
| Complejidad operativa | Una pieza de infraestructura crítica más que actualizar, vigilar y depurar |
| Recursos | Un sidecar por pod: entre 50 y 100 MiB de memoria y algo de CPU. Con 50 pods, son varios GiB |
| Latencia | Entre 1 y 5 ms por salto. Con varios saltos encadenados, se nota |
| Curva de aprendizaje | Conceptos nuevos: VirtualService, DestinationRule, AuthorizationPolicy, PeerAuthentication |
| Depuración más difícil | Un error 503 puede venir de la aplicación o del proxy. Hay que aprender a leer los logs de Envoy |
| Acoplamiento en las actualizaciones | Actualizar la malla suele implicar reiniciar todos los pods |
| Casos raros | Trabajos que terminan y el sidecar sigue vivo; protocolos que el proxy no entiende bien |
Ese último punto tiene un ejemplo directo en Rutas Norte: el CronJob informes-ocupacion termina su trabajo, pero el sidecar sigue corriendo y el Job nunca se marca como completado. Hay soluciones (contenedores sidecar nativos desde 1.29, o llamar al punto de terminación del proxy), pero es exactamente el tipo de fricción imprevista que aparece.
¿Necesita Rutas Norte una malla de servicio?
Vamos a responder con honestidad, porque es una decisión cara.
Argumentos a favor:
- Transporta datos personales entre servicios y el cifrado interno es exigible en un análisis de riesgo serio.
- La autorización L7 permitiría reglas como "solo
tienda-webpuede hacerPOST /reservas", que NetworkPolicy no puede expresar. - La identidad criptográfica elimina la suplantación de servicios.
Argumentos en contra:
- Son seis componentes. Una malla brilla con decenas o cientos de microservicios y comunicaciones complejas.
- El grafo de llamadas es casi lineal:
tienda-web→api-reservas→postgres-reservas/redis-cache. No hay una topología que justifique una capa de encaminamiento. - El equipo de plataforma es pequeño. Añadir una malla mal operada puede empeorar la seguridad (certificados caducados, políticas mal entendidas, actualizaciones aplazadas).
- Buena parte del beneficio se consigue más barato: cifrado del CNI para el tránsito, TLS en PostgreSQL para lo crítico, y NetworkPolicies estrictas para la segmentación.
Decisión para Rutas Norte: no, todavía no. El plan alternativo:
- Activar el cifrado WireGuard del CNI (una opción de configuración).
- Configurar TLS en
postgres-reservas, que es donde están los datos personales. - Mantener las NetworkPolicies estrictas con control de salida.
- Revisar la decisión cuando se cumpla alguno de estos criterios objetivos: más de quince servicios, comunicación entre varios equipos que no se coordinan, un requisito normativo explícito de mTLS, o la necesidad real de despliegues canary por porcentaje de tráfico.
Y una recomendación general que va más allá de este curso:
Una malla de servicio es una respuesta excelente a un problema que hay que tener primero. Adoptarla "porque es lo moderno", sin la complejidad que la justifica, añade riesgo operativo sin añadir seguridad neta. Si la operas mal, es peor que no tenerla.
Dicho esto, hay que conocerla, porque el día que la plataforma crezca será la respuesta correcta. Vamos con las opciones.
- Istio, Linkerd y Cilium comparados
| Istio (sidecar) | Istio (ambient) | Linkerd | Cilium Service Mesh | |
|---|---|---|---|---|
| Arquitectura | Sidecar Envoy por pod | Agente por nodo (ztunnel) + waypoint L7 opcional | Sidecar propio (linkerd2-proxy, en Rust) | eBPF en el kernel + Envoy solo para L7 |
| Memoria por pod | 50-100 MiB | Casi cero (sin sidecar) | 10-20 MiB | Casi cero para L4 |
| Latencia añadida | 2-5 ms | 1-3 ms | <1 ms | Mínima |
| Complejidad | Alta | Media | Baja | Media-alta |
| mTLS automático | Sí | Sí | Sí | Sí |
| Autorización L7 | Muy completa | Sí (con waypoint) | Básica | Sí |
| Encaminamiento avanzado | El más completo | Completo | Suficiente | Bueno |
| Multiclúster | Muy maduro | Sí | Sí | Sí |
| Requiere un CNI concreto | No | No | No | Sí: Cilium |
| Comunidad y ecosistema | El mayor | Creciente | Sólida | Creciente |
| Curva de aprendizaje | Pronunciada | Media | Suave | Media |
| Gobierno | CNCF (graduado) | CNCF | CNCF (graduado) | CNCF (graduado) |
Criterios de elección
Istio en modo sidecar si necesitas el conjunto de funciones más completo: encaminamiento sofisticado, multiclúster complejo, integración con pasarelas de API, extensiones con WebAssembly. Es la opción con más capacidad y también la que más cuesta operar. Necesitas gente dedicada.
Istio en modo ambient es la evolución que ataca el principal reproche al modelo sidecar: el coste por pod. En lugar de un proxy en cada pod, hay un agente por nodo (ztunnel) que da mTLS y L4, y solo se despliega un proxy L7 (waypoint) donde de verdad hace falta autorización de nivel 7. Reduce muchísimo el consumo y elimina el problema de los Jobs que no terminan. Si hoy tuviéramos que elegir Istio, sería este modo.
Linkerd si quieres mTLS y observabilidad con la mínima complejidad. Su proxy en Rust es notablemente ligero y su filosofía es hacer pocas cosas y hacerlas bien. Para una plataforma como Rutas Norte, si algún día hiciera falta una malla, esta sería la primera candidata: cubre lo que necesitamos (mTLS, identidad, autorización básica) sin la superficie de Istio.
Cilium Service Mesh si ya usas Cilium como CNI. Al integrar la malla en el CNI evitas una capa completa: el mTLS y las políticas L4 se aplican en eBPF, dentro del kernel, sin proxy. Solo se despliega Envoy para las políticas L7. Es la opción más eficiente, y además trae las políticas por nombre de dominio del apartado 4 y la visibilidad de Hubble del apartado 11. La contrapartida es que ata la decisión de la malla a la del CNI.
- Políticas de autorización de nivel 7
Este es el hueco concreto que NetworkPolicy no puede llenar, y conviene verlo con un ejemplo real.
El límite de L3/L4
Nuestra política actual dice: tienda-web puede conectar con api-reservas en el puerto 8080. Eso es todo lo que puede expresar. En cuanto la conexión está abierta, tienda-web puede hacer:
| Petición | ¿Debería poder? | ¿Lo impide la NetworkPolicy? |
|---|---|---|
GET /disponibilidad |
Sí | — |
POST /reservas |
Sí | — |
GET /admin/clientes |
No | No |
DELETE /reservas/12345 |
No | No |
GET /metrics |
Discutible | No |
Una política L7 sí puede distinguir. Veamos cómo se expresaría con Istio, tomando el ejemplo del enunciado: solo api-reservas puede hacer POST /reservas.
# Ejemplo de política L7 con Istio (solo si se adoptara una malla)
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: api-reservas-autorizacion
namespace: rutas-norte-pro
spec:
selector:
matchLabels:
app: api-reservas
action: ALLOW
rules:
# Regla 1: tienda-web puede consultar disponibilidad y crear reservas
- from:
- source:
# IDENTIDAD CRIPTOGRÁFICA, no una etiqueta ni una IP
principals:
- "cluster.local/ns/rutas-norte-pro/sa/tienda-web"
to:
- operation:
methods: ["GET"]
paths: ["/disponibilidad", "/lineas", "/salud", "/preparado"]
- operation:
methods: ["POST"]
paths: ["/reservas"]
# Regla 2: solo el worker de notificaciones consulta reservas pendientes
- from:
- source:
principals:
- "cluster.local/ns/rutas-norte-pro/sa/worker-notificaciones"
to:
- operation:
methods: ["GET"]
paths: ["/reservas/pendientes"]
# Regla 3: Prometheus solo puede leer las métricas
- from:
- source:
principals:
- "cluster.local/ns/monitorizacion/sa/prometheus"
to:
- operation:
methods: ["GET"]
paths: ["/metrics"]Con action: ALLOW, todo lo que no encaje con ninguna regla se deniega. Es el mismo principio de denegación por defecto de las NetworkPolicies, ahora aplicado a métodos y rutas HTTP.
Observa la línea clave:
Eso no es una etiqueta ni una IP: es la identidad SPIFFE que la malla verifica criptográficamente en el saludo mTLS. Un pod no puede reclamar esa identidad sin tener la clave privada correspondiente. Y fíjate en que se basa en la ServiceAccount, cerrando el círculo con 03-06 y 08-01: la ServiceAccount pasa de ser solo la identidad ante la API a ser también la identidad ante los demás servicios.
La comparación completa
| NetworkPolicy (04-06) | AuthorizationPolicy (malla) | |
|---|---|---|
| Nivel | L3/L4: IP y puerto | L7: método, ruta, cabeceras |
| Identidad | Etiquetas del pod (suplantables) | Certificado criptográfico |
| Cifrado | No | Sí (mTLS) |
| Registro de denegaciones | No | Sí |
| Requiere instalar | Nada (con CNI compatible) | Una malla completa |
| Coste operativo | Bajo | Alto |
Las dos son complementarias, no alternativas. La NetworkPolicy es la primera línea y la más barata; la política L7 es la afinación. Si tuvieras que quedarte con una, quédate con la NetworkPolicy: sin ella, la política L7 protege un servicio al que cualquiera puede seguir conectándose por otras vías.
La alternativa sin malla
Sin malla, la autorización L7 la hace la propia aplicación. api-reservas puede validar en su código quién le llama:
# La aplicación valida un token de servicio inyectado como Secret
env:
- name: TOKEN_SERVICIO_ESPERADO
valueFrom:
secretKeyRef:
name: tokens-internos
key: tienda-webFunciona y para seis servicios es perfectamente razonable, pero tiene inconvenientes reales: hay que implementarlo en cada servicio, rotar los tokens a mano, y si alguien lee el Secret (RBAC, 08-01) puede suplantar al servicio. El mTLS de la malla resuelve las tres cosas automáticamente. Es exactamente el tipo de coste que va creciendo con el número de servicios y que, llegado un punto, justifica la malla.
- Protección del perímetro
Todo lo anterior protege el interior. Ahora, la puerta de entrada.
Exposición mínima: por qué no hay NodePort en producción
En 04-02 vimos los tipos de Service. Repasemos su implicación de seguridad:
| Tipo | Exposición | ¿En producción? |
|---|---|---|
ClusterIP |
Solo dentro del clúster | Sí: por defecto para todo |
NodePort |
Un puerto (30000-32767) en todos los nodos | No |
LoadBalancer |
Un balanceador del proveedor | Solo para el Ingress |
| Ingress | HTTP/HTTPS por nombre y ruta | Sí: única entrada |
Por qué NodePort no vale en producción:
- Abre el puerto en todos los nodos, incluidos los que no ejecutan el pod.
- Se salta el Ingress, y con él el TLS (04-05), el WAF y la limitación de peticiones.
- El puerto está en un rango alto y poco convencional, difícil de proteger con un cortafuegos convencional.
- No tiene nombre: cualquiera que alcance la IP del nodo llega al servicio.
Y recuerda de 08-02 que hostPort es todavía peor, porque ni siquiera hay un Service delante.
La regla de Rutas Norte: un único punto de entrada, el balanceador del Ingress. Todo lo demás es ClusterIP.
# Auditoría: ¿hay algún servicio expuesto que no debería?
kubectl get svc -A -o json | jq -r '
.items[] | select(.spec.type == "NodePort" or .spec.type == "LoadBalancer")
| "\(.spec.type)\t\(.metadata.namespace)/\(.metadata.name)\t" +
([.spec.ports[] | "\(.port)->\(.nodePort // "-")"] | join(","))'Un único LoadBalancer, el del Ingress. Cualquier otra línea en esa salida es una pregunta que hay que responder.
WAF y limitación de peticiones
Un cortafuegos de aplicación web (WAF) inspecciona las peticiones HTTP y bloquea patrones de ataque conocidos: inyección SQL, ejecución de scripts entre sitios, recorrido de directorios. Con ingress-nginx se puede activar ModSecurity con el conjunto de reglas OWASP:
# k8s/base/ingress/configmap-waf.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
enable-modsecurity: "true"
enable-owasp-modsecurity-crs: "true"
modsecurity-snippet: |
SecRuleEngine On
SecRequestBodyAccess On
SecRequestBodyLimit 5242880
SecAuditEngine RelevantOnly
SecAuditLogParts ABIJDEFHZ
# Nivel de paranoia 1: pocos falsos positivos. Subir con cuidado.
SecAction "id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=1"Advertencia seria sobre los WAF: un WAF mal ajustado bloquea peticiones legítimas. Si tienda-web deja de poder crear reservas porque una regla del WAF considera sospechoso un nombre con apóstrofe, has creado una incidencia. El procedimiento correcto es idéntico al del PSA en 08-03:
SecRuleEngine DetectionOnly: registra pero no bloquea.- Analizar los falsos positivos durante semanas de tráfico real.
- Ajustar las reglas problemáticas.
- Solo entonces,
SecRuleEngine On.
Y la limitación de peticiones, por anotación en el Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rutas-norte
namespace: rutas-norte-pro
annotations:
cert-manager.io/cluster-issuer: letsencrypt-produccion # de 04-05
# Limitación de peticiones
nginx.ingress.kubernetes.io/limit-rps: "20"
nginx.ingress.kubernetes.io/limit-connections: "10"
nginx.ingress.kubernetes.io/limit-burst-multiplier: "3"
# Tamaño máximo del cuerpo: evita agotar memoria
nginx.ingress.kubernetes.io/proxy-body-size: "2m"
# Tiempos de espera: evita conexiones que se quedan colgadas
nginx.ingress.kubernetes.io/proxy-read-timeout: "30"
nginx.ingress.kubernetes.io/proxy-send-timeout: "30"
# Cabeceras de seguridad
nginx.ingress.kubernetes.io/configuration-snippet: |
more_set_headers "X-Content-Type-Options: nosniff";
more_set_headers "X-Frame-Options: DENY";
more_set_headers "Referrer-Policy: strict-origin-when-cross-origin";
more_set_headers "Permissions-Policy: geolocation=(), microphone=(), camera=()";
more_set_headers "Strict-Transport-Security: max-age=31536000; includeSubDomains";
spec:
ingressClassName: nginx
tls:
- hosts: [www.rutasnorte.example]
secretName: rutasnorte-tls
rules:
- host: www.rutasnorte.example
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: tienda-web
port: { number: 80 }
- path: /api
pathType: Prefix
backend:
service:
name: api-reservas
port: { number: 8080 }Las cabeceras, una a una:
| Cabecera | Qué mitiga |
|---|---|
X-Content-Type-Options: nosniff |
Que el navegador adivine el tipo de contenido y ejecute algo como script |
X-Frame-Options: DENY |
Que la página se incruste en un marco de otro sitio (clickjacking) |
Referrer-Policy |
Que se filtren URL internas al navegar a sitios externos |
Permissions-Policy |
Acceso no solicitado a cámara, micrófono o ubicación |
Strict-Transport-Security |
Que un cliente vuelva a conectar por HTTP sin cifrar |
Un detalle sobre HSTS: max-age=31536000 son 365 días, y los navegadores lo recuerdan. Si algún día tu TLS deja de funcionar, los clientes que ya visitaron el sitio no podrán entrar por HTTP como alternativa. Es lo deseable desde el punto de vista de seguridad, pero hay que ser consciente antes de activarlo.
Protección contra denegación de servicio
| Nivel del ataque | Dónde se mitiga |
|---|---|
| Volumétrico (saturar el ancho de banda) | Fuera del clúster: proveedor de nube o CDN |
| Agotamiento de conexiones | Balanceador y limit-connections |
| Peticiones costosas | limit-rps, y en la aplicación: paginación y tiempos de espera |
| Agotamiento de recursos internos | resources.limits (03-04) y autoescalado (módulo 9) |
Un punto importante que se olvida: el autoescalado no es una defensa contra la denegación de servicio, es una forma de pagarla. Si api-reservas escala a 50 réplicas ante un ataque, has convertido una caída en una factura. El límite superior del HPA (09-01) es también un control de seguridad y de coste.
- Seguridad del plano de control
Todo lo anterior protege las cargas. El plano de control es el objetivo de mayor valor: quien lo controla, controla todo el clúster.
Acceso al apiserver
| Control | Cómo |
|---|---|
| No exponerlo a internet | Punto final privado, o lista de IP permitidas |
| Autenticación fuerte | OIDC con segundo factor, nunca certificados compartidos |
| Deshabilitar el acceso anónimo | --anonymous-auth=false |
| Registro de auditoría | Política de auditoría (08-06) |
| Limitación de peticiones | --max-requests-inflight, prioridad y equidad de la API |
Una IP privada es buena señal. Si vieras una IP pública sin restricción de origen, es un hallazgo de seguridad de primer nivel.
En Kubernetes gestionado (10-06), los tres grandes proveedores permiten restringir el acceso al punto final. Es de las primeras cosas que hay que configurar en un clúster nuevo.
etcd
etcd guarda todo el estado del clúster, incluidos los Secrets. Como dijimos en 03-02, si etcd no está cifrado, los Secrets están en claro en el disco.
Cifrado en reposo:
# /etc/kubernetes/cifrado/config.yaml (en los nodos del plano de control)
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
- configmaps
providers:
# KMS externo: la clave maestra vive fuera del clúster. Preferible.
- kms:
apiVersion: v2
name: kms-rutasnorte
endpoint: unix:///var/run/kmsplugin/socket.sock
cachesize: 1000
- identity: {} # sin cifrar: necesario para poder leer lo antiguoDetalle importante: el orden de los proveedores importa. El primero se usa para escribir; todos se usan para leer. Tener identity: {} al final permite leer los objetos que se escribieron antes de activar el cifrado. Y tras activarlo, hay que reescribirlos:
Sin ese paso, los Secrets antiguos siguen en claro en etcd para siempre.
Otros controles de etcd:
| Control | Por qué |
|---|---|
| TLS mutuo entre etcd y el apiserver | Nadie más debe poder hablar con etcd |
| Acceso restringido al puerto 2379 | Solo desde el plano de control |
| Copias de seguridad cifradas | Una copia de etcd sin cifrar es una copia de todos los Secrets |
| Custodia de las copias | Con la misma protección que la base de datos de clientes |
Ese punto de las copias es fácil de pasar por alto y muy grave. En 05-06 hablamos de Velero y de los datos personales; una copia de etcd contiene, además, todas las credenciales del clúster. Debe cifrarse, almacenarse con acceso restringido y registrado, y probarse su restauración periódicamente.
El kubelet
Cada nodo ejecuta un kubelet con una API en el puerto 10250. Sin protección, permite listar pods, leer logs y ejecutar comandos en cualquier contenedor de ese nodo.
| Ajuste | Valor correcto | Qué evita |
|---|---|---|
--anonymous-auth |
false |
Peticiones sin credenciales |
--authorization-mode |
Webhook |
Que cualquier identidad válida pueda todo |
--read-only-port |
0 |
El puerto 10255, sin autenticación, expone metadatos de los pods |
--protect-kernel-defaults |
true |
Que el kubelet modifique parámetros del kernel |
Comprobación:
# Debe responder 401: la API del kubelet no acepta peticiones anónimas
kubectl get --raw /api/v1/nodes/nodo-pro-01/proxy/pods -v6 2>&1 | grep -i "response"Y una NetworkPolicy o regla de cortafuegos que impida a los pods alcanzar el puerto 10250 de los nodos es una defensa muy rentable, porque cierra una vía de escalada conocida.
Recuerda de 08-01 que el permiso get nodes/proxy es de nivel crítico precisamente por esto: permite hablar con el kubelet y, a través de él, ejecutar en cualquier pod del nodo.
Aislamiento de los nodos del plano de control
Los nodos del plano de control no deben ejecutar cargas de aplicación. Kubernetes lo consigue con un taint (06-05):
Ese taint impide que se planifiquen pods normales ahí. La razón es directa: un pod en un nodo del plano de control está a un fallo de distancia de los certificados del clúster y de la base de datos de etcd.
Y hay que verificar que nadie lo ha tolerado por comodidad:
kubectl get pods -A -o json | jq -r '
.items[]
| select(.spec.tolerations[]? |
.key == "node-role.kubernetes.io/control-plane" or
(.operator == "Exists" and (.key // "") == ""))
| "\(.metadata.namespace)/\(.metadata.name)"'Solo componentes del sistema que legítimamente corren en todos los nodos. Si apareciera aquí un pod de rutas-norte-pro, hay que investigar por qué se puso esa tolerancia. Ojo con la tolerancia universal (operator: Exists sin key): tolera todos los taints, incluido el del plano de control, y a menudo se pone sin darse cuenta.
- Registro y visibilidad del tráfico
Volvemos al segundo límite que señalamos en 04-06: las NetworkPolicies no registran nada.
Qué significa exactamente
Cuando una NetworkPolicy deniega una conexión, el paquete se descarta en el kernel. No hay evento de Kubernetes, no hay log, no hay métrica. Desde el pod origen, la conexión simplemente agota su tiempo de espera.
Las consecuencias son dos, y las dos son malas:
Para operar: depurar "por qué mi servicio no conecta" es doloroso. Puede ser DNS, una política, un Service mal configurado o la aplicación. Nada te dice cuál.
Para la seguridad: si alguien compromete un pod y empieza a sondear la red interna, generará decenas de conexiones denegadas y no se enterará nadie. Justo la señal más clara de un movimiento lateral es invisible.
Hubble: observabilidad de flujos con Cilium
Hubble, el componente de observabilidad de Cilium, resuelve esto aprovechando que Cilium ya inspecciona cada paquete en eBPF.
helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values \
--set hubble.enabled=true \
--set hubble.relay.enabled=true \
--set hubble.ui.enabled=true \
--set hubble.metrics.enabled="{dns,drop,tcp,flow,port-distribution,httpV2}"Ver flujos en tiempo real:
TIMESTAMP SOURCE DESTINATION TYPE VERDICT
Aug 6 09:14:22.481 rutas-norte-pro/tienda-web-x2 rutas-norte-pro/api-reservas-a1:8080 to-endpoint FORWARDED
Aug 6 09:14:22.503 rutas-norte-pro/api-reservas-a1 rutas-norte-pro/postgres-reservas-0:5432 to-endpoint FORWARDED
Aug 6 09:14:23.117 rutas-norte-pro/api-reservas-a1 rutas-norte-pro/redis-cache-0:6379 to-endpoint FORWARDEDY lo que de verdad importa, las denegaciones:
TIMESTAMP SOURCE DESTINATION TYPE VERDICT REASON
Aug 6 03:47:11.229 rutas-norte-pro/tienda-web-x2k4 rutas-norte-pro/postgres-reservas-0:5432 to-endpoint DROPPED Policy denied
Aug 6 03:47:11.884 rutas-norte-pro/tienda-web-x2k4 rutas-norte-pro/redis-cache-0:6379 to-endpoint DROPPED Policy denied
Aug 6 03:47:12.401 rutas-norte-pro/tienda-web-x2k4 203.0.113.44:443 to-stack DROPPED Policy denied
Aug 6 03:47:13.055 rutas-norte-pro/tienda-web-x2k4 198.51.100.7:22 to-stack DROPPED Policy deniedLéelo con atención, porque esa salida cuenta una historia completa. tienda-web —el componente más expuesto— ha intentado, en cinco segundos: conectar con la base de datos de clientes, conectar con la caché, salir a internet por HTTPS y salir a internet por SSH. tienda-web no hace nada de eso en su funcionamiento normal. Es exactamente el patrón de alguien explorando qué puede alcanzar desde un pod comprometido.
Las políticas hicieron su trabajo: todo DROPPED. Pero sin Hubble, esto habría pasado completamente desapercibido.
Preguntas que se pueden responder tras un incidente
| Pregunta | Consulta |
|---|---|
| ¿Qué intentó alcanzar este pod? | hubble observe --pod <nombre> --last 1000 |
| ¿Alguien salió a internet desde producción? | hubble observe --namespace rutas-norte-pro --to-fqdn "*" |
| ¿Cuándo empezaron los intentos anómalos? | hubble observe --verdict DROPPED --since 24h |
| ¿Quién habló con la base de datos? | hubble observe --to-pod rutas-norte-pro/postgres-reservas-0 |
| ¿Qué rutas HTTP se pidieron? | hubble observe --protocol http --http-path "/admin*" |
Convertirlo en alertas
Hubble expone métricas Prometheus, así que se integra directamente con el módulo 7:
# k8s/base/monitorizacion/reglas-red.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: alertas-seguridad-red
namespace: monitorizacion
labels:
app.kubernetes.io/part-of: rutas-norte
spec:
groups:
- name: seguridad-red
rules:
- alert: ConexionesDenegadasAnomalas
expr: |
sum by (source_pod, source_namespace) (
rate(hubble_drop_total{
source_namespace="rutas-norte-pro",
reason="POLICY_DENIED"
}[5m])
) > 0.5
for: 5m
labels:
severity: warning
equipo: plataforma
annotations:
summary: >-
El pod {{ $labels.source_pod }} genera conexiones denegadas de forma
sostenida
description: >-
Más de 0,5 conexiones denegadas por segundo durante 5 minutos.
Puede ser un despliegue con una política mal configurada, o un pod
comprometido sondeando la red. Investiga con:
hubble observe --pod {{ $labels.source_namespace }}/{{ $labels.source_pod }} --verdict DROPPED
runbook_url: https://wiki.rutasnorte.example/runbooks/conexiones-denegadas
- alert: SalidaInesperadaDesdeProduccion
expr: |
sum by (source_pod) (
rate(hubble_flows_processed_total{
source_namespace="rutas-norte-pro",
destination_namespace="",
verdict="FORWARDED"
}[5m])
) > 0
for: 2m
labels:
severity: critical
equipo: plataforma
annotations:
summary: >-
Tráfico saliente a internet desde {{ $labels.source_pod }}
description: >-
Solo api-reservas (pasarela de pagos) y worker-notificaciones (SMTP)
deben salir a internet. Cualquier otro origen es una posible vía de
exfiltración de datos de clientes. Escalar de inmediato.Esa segunda alerta es de las más valiosas de toda la plataforma: avisa de una posible exfiltración de datos personales en curso. La ruta a Alertmanager y su encaminamiento son las del módulo 7; lo único nuevo es la fuente de la métrica.
Alternativas sin Cilium
| Herramienta | Qué aporta |
|---|---|
| Registro de flujos del proveedor (VPC Flow Logs) | Flujos a nivel de red virtual, sin identidad de pod |
| Calico Enterprise | Registro de flujos con contexto de Kubernetes (producto comercial) |
| Falco con reglas de red | Conexiones inesperadas observadas desde las llamadas al sistema (08-06) |
| Registro del proxy de salida | Destinos externos solicitados, con nombre de dominio |
Para Rutas Norte, sin Cilium hoy, la combinación práctica es: registro del proxy de salida para el tráfico externo (que es el crítico) y Falco para las conexiones anómalas, que veremos en la lección siguiente.
- Confianza cero aplicada a Rutas Norte
La confianza cero (zero trust) es un modelo que se resume en una frase: nunca confíes, verifica siempre. Nada se considera de confianza por su ubicación en la red.
Los principios y su traducción
| Principio | En Rutas Norte |
|---|---|
| Nada es de confianza por su ubicación | Un pod en rutas-norte-pro no tiene acceso privilegiado por estar ahí |
| Verificar identidad en cada petición | ServiceAccounts (03-06) y RBAC (08-01); mTLS si hubiera malla |
| Mínimo privilegio | RBAC ajustado y políticas de red por componente |
| Asumir el compromiso | Microsegmentación y control de salida limitan el daño |
| Cifrar en tránsito | TLS en el Ingress, WireGuard en el CNI, TLS en PostgreSQL |
| Registrar y verificar todo | Hubble, auditoría del apiserver (08-06), Falco |
La idea de asumir el compromiso es la que más cambia el diseño. No se trata de "cómo evito que entren", sino de "cuando entren, ¿qué pueden hacer y en cuánto tiempo me entero?".
La tabla de defensa completa
| Capa | Mecanismo | Ataque que mitiga | Estado |
|---|---|---|---|
| Perímetro | WAF + OWASP CRS | Inyección SQL, XSS, recorrido de directorios | Por implantar |
| Perímetro | Limitación de peticiones | Abuso, fuerza bruta, denegación de servicio a nivel de aplicación | Por implantar |
| Perímetro | Solo Ingress, sin NodePort | Exposición accidental de servicios internos | Hecho |
| Transporte | TLS con cert-manager | Escucha del tráfico cliente-plataforma | Hecho (04-05) |
| Transporte | Cabeceras de seguridad | Clickjacking, degradación a HTTP, filtrado de URL | Por implantar |
| Transporte | WireGuard en el CNI | Escucha del tráfico entre nodos | Recomendado |
| Transporte | TLS en PostgreSQL | Escucha de datos personales en tránsito | Recomendado |
| Segmentación | deny-all en los tres entornos |
Movimiento lateral tras un compromiso | Hecho (04-06) + Kyverno (08-03) |
| Segmentación | Políticas por componente | Alcance del movimiento lateral | Hecho (04-06) |
| Segmentación | Control de salida por ipBlock |
Exfiltración de datos de clientes | Prioritario |
| Identidad | ServiceAccounts dedicadas | Suplantación de procesos ante la API | Hecho (03-06) |
| Identidad | mTLS con malla | Suplantación entre servicios | Aplazado, con criterios |
| Autorización | RBAC de mínimo privilegio | Abuso de credenciales legítimas | Hecho (08-01) |
| Autorización | Políticas L7 | Acceso a rutas no autorizadas de la API | Aplazado |
| Endurecimiento | securityContext restrictivo |
Escape del contenedor al nodo | Hecho (08-02) |
| Endurecimiento | PSA + Kyverno | Que alguien despliegue algo inseguro | Hecho (08-03) |
| Plano de control | apiserver privado, etcd cifrado | Compromiso total del clúster | Por revisar |
| Plano de control | kubelet autenticado, sin puerto de solo lectura | Ejecución en pods vía kubelet | Por revisar |
| Visibilidad | Hubble o registro del proxy | Ceguera ante el sondeo interno y la exfiltración | Prioritario |
| Visibilidad | Alertas sobre denegaciones | Detección tardía de un incidente | Por implantar |
Las tres prioridades que salen de esta tabla, por orden: control de salida, visibilidad del tráfico y revisión del plano de control. Todas atacan el mismo escenario: alguien ya está dentro.
Errores Comunes y Consejos
Escribir solo políticas de Ingress. Si policyTypes no incluye Egress, la salida está abierta y con ella la vía de exfiltración. Es el error más grave y más frecuente de esta lección.
Activar Egress y olvidar el DNS. Todo deja de funcionar de una forma muy confusa: los nombres no resuelven y parece un problema de Service. La regla hacia kube-dns en el puerto 53, UDP y TCP, es la primera que hay que escribir.
Usar ipBlock: 0.0.0.0/0 sin except. Incluye la red interna del clúster, así que la política es mucho más permisiva de lo que parece.
Confiar en que los ipBlock de un proveedor externo no cambian. Cambian. Programa una verificación mensual o usa un proxy con lista de dominios.
Creer que el contenedor embajador aísla la red. Comparte el namespace de red con el resto del pod. Es un patrón de arquitectura, no un límite de seguridad.
Escribir políticas por namespace en lugar de por componente. Reproduce la red plana dentro del namespace. El coste de hacerlo bien son cinco políticas más.
Dejar rutas-norte-dev y rutas-norte-pre sin políticas. La red del clúster es plana entre namespaces, y una política que solo se prueba en producción se prueba en el peor momento.
Adoptar una malla de servicio sin necesitarla. Es infraestructura crítica: mal operada, empeora la seguridad. Define criterios objetivos para revisar la decisión.
Activar un WAF directamente en modo bloqueo. Bloqueará peticiones legítimas y tendrás una incidencia. DetectionOnly primero, semanas de análisis, y luego bloqueo.
Usar NodePort en producción "porque es más rápido de configurar". Se salta el TLS, el WAF y la limitación de peticiones, y abre el puerto en todos los nodos.
Olvidar que el autoescalado no defiende de la denegación de servicio. Convierte una caída en una factura. Pon un límite superior al HPA.
Cifrar etcd y no reescribir los Secrets existentes. Los antiguos siguen en claro. kubectl get secrets -A -o json | kubectl replace -f -.
No cifrar las copias de seguridad de etcd. Una copia de etcd contiene todos los Secrets del clúster.
Tolerancias universales. Un operator: Exists sin key tolera todos los taints, incluido el del plano de control, y suele ponerse sin darse cuenta.
Consejo de oro: dibuja el diagrama de qué habla con qué y comprueba que las políticas lo reproducen exactamente, ni una flecha más. Después, haz la pregunta clave: si compromenten este pod, ¿a dónde puede llegar y por dónde pueden sacar los datos? Si la respuesta a la segunda parte no es "a ningún sitio", hay trabajo pendiente en el egreso.
Ejercicios
Ejercicio 1: cerrar la salida de los componentes que no la necesitan
Cuatro de los seis componentes de Rutas Norte no necesitan salir a internet: tienda-web, postgres-reservas, redis-cache e informes-ocupacion. Escribe una única NetworkPolicy que les permita solo lo imprescindible dentro del clúster y les cierre por completo la salida a internet.
Ten en cuenta que:
- Todos necesitan DNS.
postgres-reservaseinformes-ocupaciondeben poder hablar entre sí (el CronJob consulta la base de datos).tienda-webdebe poder llegar aapi-reservas.- Todos deben poder ser consultados por Prometheus.
Incluye la orden que verificaría que la salida está efectivamente cerrada.
Ejercicio 2: decidir sobre la malla de servicio
La dirección de Rutas Norte plantea desplegar Istio "porque es lo que usan las empresas grandes". Prepara una respuesta técnica de una página que incluya:
- Qué problemas concretos resolvería en la plataforma actual.
- Qué costaría, de forma cuantificada.
- Qué alternativas más baratas cubren parte del beneficio.
- Criterios objetivos y medibles para revisar la decisión.
- Una recomendación clara.
Ejercicio 3: investigar un incidente con los flujos de red
A las 03:47 salta la alerta ConexionesDenegadasAnomalas. Consultas Hubble y obtienes:
TIMESTAMP SOURCE DESTINATION TYPE VERDICT REASON
Aug 6 03:47:09.112 rutas-norte-pro/worker-notif-k9x2 rutas-norte-pro/api-reservas-a1:8080 to-endpoint FORWARDED
Aug 6 03:47:11.229 rutas-norte-pro/worker-notif-k9x2 rutas-norte-pro/postgres-reservas-0:5432 to-endpoint FORWARDED
Aug 6 03:47:11.884 rutas-norte-pro/worker-notif-k9x2 rutas-norte-pro/redis-cache-0:6379 to-endpoint DROPPED Policy denied
Aug 6 03:47:12.401 rutas-norte-pro/worker-notif-k9x2 192.0.2.55:443 to-stack DROPPED Policy denied
Aug 6 03:47:12.902 rutas-norte-pro/worker-notif-k9x2 192.0.2.55:8443 to-stack DROPPED Policy denied
Aug 6 03:47:13.455 rutas-norte-pro/worker-notif-k9x2 192.0.2.55:53 to-stack DROPPED Policy denied
Aug 6 03:47:20.001 rutas-norte-pro/worker-notif-k9x2 rutas-norte-pro/postgres-reservas-0:5432 to-endpoint FORWARDED- ¿Qué está ocurriendo? Justifica tu lectura línea por línea.
- ¿Qué capas de defensa han funcionado y cuáles no?
- Enumera las acciones inmediatas, en orden de prioridad.
- ¿Qué habría pasado sin control de salida? ¿Y sin Hubble?
Soluciones
Solución 1
# k8s/base/red/salida-cerrada.yaml
# Política de salida para los componentes que NO deben alcanzar internet.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: sin-salida-a-internet
namespace: rutas-norte-pro
labels:
app.kubernetes.io/part-of: rutas-norte
annotations:
seguridad.rutasnorte.example/justificacion: >-
tienda-web, postgres-reservas, redis-cache e informes-ocupacion no tienen
ninguna necesidad de alcanzar internet. Cerrar su salida elimina la vía de
exfiltración de datos personales para cuatro de los seis componentes.
spec:
podSelector:
matchExpressions:
- key: app
operator: In
values:
- tienda-web
- postgres-reservas
- redis-cache
- informes-ocupacion
policyTypes: [Egress]
egress:
# 1. DNS: imprescindible para todos
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
# 2. tienda-web -> api-reservas
# (el podSelector de esta regla filtra el destino, no el origen;
# los demás componentes simplemente no usan este permiso)
- to:
- podSelector:
matchLabels:
app: api-reservas
ports:
- { protocol: TCP, port: 8080 }
# 3. informes-ocupacion -> postgres-reservas
- to:
- podSelector:
matchLabels:
app: postgres-reservas
ports:
- { protocol: TCP, port: 5432 }
# NO HAY ninguna regla con ipBlock: la salida a internet está cerrada.Y la política de entrada para que Prometheus pueda raspar métricas (es Ingress, complementaria):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: permitir-prometheus
namespace: rutas-norte-pro
spec:
podSelector: {}
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitorizacion
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- { protocol: TCP, port: 9090 }Una observación importante sobre la primera política: al usar un podSelector con matchExpressions que agrupa cuatro componentes, todos comparten las mismas reglas de salida. Eso significa que redis-cache técnicamente podría conectar con api-reservas:8080, aunque no lo haga.
Es un compromiso consciente entre concisión y precisión. La versión estrictamente correcta sería una política por componente, y es lo que recomendaríamos en producción siguiendo el principio 2 del apartado 2. Esta versión agrupada es aceptable porque el objetivo declarado del ejercicio —cerrar la salida a internet— se cumple igualmente, y ninguno de los permisos internos concedidos de más da acceso a datos personales. Documentar ese razonamiento es tan importante como el YAML.
Verificación:
#!/usr/bin/env bash
NS=rutas-norte-pro
for objetivo in deploy/tienda-web statefulset/postgres-reservas statefulset/redis-cache; do
echo -n "$objetivo -> internet: "
if kubectl exec -n "$NS" "$objetivo" -- \
timeout 5 sh -c 'wget -q -O- --timeout=4 https://ejemplo-externo.example' \
>/dev/null 2>&1; then
echo "ALCANZA <-- FALLO"
else
echo "bloqueado (correcto)"
fi
echo -n "$objetivo -> DNS: "
kubectl exec -n "$NS" "$objetivo" -- \
timeout 5 nslookup api-reservas >/dev/null 2>&1 \
&& echo "resuelve (correcto)" || echo "NO RESUELVE <-- FALLO"
donedeploy/tienda-web -> internet: bloqueado (correcto)
deploy/tienda-web -> DNS: resuelve (correcto)
statefulset/postgres-reservas -> internet: bloqueado (correcto)
statefulset/postgres-reservas -> DNS: resuelve (correcto)
statefulset/redis-cache -> internet: bloqueado (correcto)
statefulset/redis-cache -> DNS: resuelve (correcto)Comprobar el DNS es tan importante como comprobar el bloqueo: una política de salida que bloquea internet y el DNS rompe la plataforma sin que sea evidente por qué.
Solución 2
Informe técnico: adopción de una malla de servicio en la plataforma Rutas Norte
1. Qué problemas resolvería
| Problema real | ¿Lo resuelve? | ¿Es urgente hoy? |
|---|---|---|
| Tráfico interno sin cifrar, incluidos datos personales | Sí, mTLS automático | Sí, es un riesgo real |
| Un servicio no puede verificar criptográficamente quién le llama | Sí, identidad SPIFFE | Moderado: hay 6 servicios, todos internos y controlados |
| No hay autorización por ruta HTTP | Sí, AuthorizationPolicy |
Bajo: el grafo de llamadas es casi lineal |
| Reintentos y tiempos de espera duplicados en cada servicio | Sí | Bajo: ya está implementado |
| Falta observabilidad de latencia entre servicios | Parcialmente | Bajo: el módulo 7 ya cubre lo esencial |
| Despliegues canary por porcentaje de tráfico | Sí | Bajo: hoy se hace por réplicas (11-04) |
De seis problemas, uno es urgente (cifrado) y cinco son mejoras deseables sin urgencia.
2. Coste cuantificado
| Concepto | Estimación |
|---|---|
| Memoria de sidecars | 6 componentes × ~15 pods × 80 MiB ≈ 1,2 GiB de memoria adicional |
| CPU de sidecars | ~0,05 núcleos por pod ≈ 0,75 núcleos |
| Latencia añadida | 2-5 ms por salto; en el camino tienda-web→api-reservas→postgres, hasta 10 ms por petición |
| Implantación inicial | 3-4 semanas de una persona de plataforma |
| Operación continua | ~1 día al mes: actualizaciones, certificados, diagnóstico |
| Formación del equipo | 2 semanas para 4 personas |
| Riesgo de incidencia | Medio-alto los primeros meses: es la causa más común de "el servicio devuelve 503 y no sé por qué" |
Con Istio en modo ambient o con Linkerd, la memoria y la latencia bajarían sustancialmente. La complejidad operativa y el riesgo de los primeros meses no.
3. Alternativas más baratas
| Alternativa | Cubre | Coste |
|---|---|---|
| WireGuard en el CNI | Cifrado de todo el tráfico entre nodos | Una opción de configuración; horas de trabajo |
TLS en postgres-reservas |
Cifrado del tramo con datos personales, incluso dentro del mismo nodo | 1-2 días |
| NetworkPolicies estrictas (ya hechas) | Segmentación L3/L4 | Hecho |
| Tokens de servicio en la aplicación | Autenticación básica entre servicios | 2-3 días por servicio |
| Hubble o registro del proxy de salida | Visibilidad de flujos | 1 semana |
Las dos primeras juntas cubren el 80 % del beneficio de seguridad por menos del 5 % del coste.
4. Criterios objetivos para revisar la decisión
Se reabrirá la evaluación cuando se cumpla cualquiera de estos:
- La plataforma supere los 15 servicios con comunicación entre ellos.
- Más de tres equipos desplieguen en el clúster sin coordinación diaria.
- Aparezca un requisito normativo o contractual explícito de mTLS entre servicios (por ejemplo, una certificación de un cliente corporativo).
- Se necesiten despliegues canary por porcentaje de tráfico que no se puedan aproximar con réplicas.
- Se necesite encaminamiento entre varios clústeres (11-05).
- El equipo de plataforma alcance 5 personas o más, con capacidad para dedicar una a la malla.
5. Recomendación
No adoptar una malla de servicio ahora. En su lugar, en este orden:
- Activar WireGuard en el CNI (esta semana).
- Configurar TLS en
postgres-reservas(este trimestre). - Implantar visibilidad de flujos (este trimestre).
- Revisar la decisión cada seis meses contra los criterios anteriores.
Si en el futuro se adopta, la primera candidata sería Linkerd por su relación entre beneficio y complejidad, o Istio en modo ambient si para entonces se necesita encaminamiento avanzado.
Nota final: esta recomendación es una valoración de arquitectura basada en el tamaño y la topología actuales. Debe validarla el profesional de seguridad de la organización, especialmente en lo relativo al cifrado del tráfico con datos personales, donde el responsable de cumplimiento normativo tiene la última palabra sobre qué nivel de protección es exigible.
Solución 3
1. Qué está ocurriendo, línea por línea
| Hora | Flujo | Lectura |
|---|---|---|
| 03:47:09 | worker-notif → api-reservas:8080 FORWARDED |
Anómalo. El worker lee de la base de datos, no llama a la API. La política lo permite (probablemente un permiso amplio de más), pero no es su comportamiento normal |
| 03:47:11 | worker-notif → postgres:5432 FORWARDED |
Legítimo por sí solo: el worker consulta reservas pendientes |
| 03:47:11 | worker-notif → redis-cache:6379 DROPPED |
Anómalo. El worker no usa la caché. Es un sondeo |
| 03:47:12 | worker-notif → 192.0.2.55:443 DROPPED |
Muy grave. Intento de salida a una IP externa que no es la pasarela de pagos ni el SMTP |
| 03:47:12 | worker-notif → 192.0.2.55:8443 DROPPED |
Reintento en otro puerto: busca una salida |
| 03:47:13 | worker-notif → 192.0.2.55:53 DROPPED |
Reintento en el puerto DNS: una técnica clásica para atravesar cortafuegos permisivos |
| 03:47:20 | worker-notif → postgres:5432 FORWARDED |
Lo más preocupante: tras fallar todas las salidas, vuelve a la base de datos |
La secuencia completa —cuatro segundos, tres puertos distintos hacia la misma IP externa, sondeo de servicios internos, y vuelta a la base de datos— no corresponde a ningún comportamiento legítimo de worker-notificaciones. La firma es la de un proceso comprometido buscando por dónde sacar información, y la última línea sugiere que sigue accediendo a datos.
Un detalle temporal importante: son las 03:47. El CronJob informes-ocupacion corre a las 03:00. Habrá que descartar que haya relación.
2. Capas que funcionaron y que no
| Capa | Resultado |
|---|---|
Control de salida (Egress) |
Funcionó. Los tres intentos de salir a internet fueron bloqueados. Esta es la capa que ha impedido la fuga de datos |
| Microsegmentación | Funcionó parcialmente. Bloqueó Redis, pero permitió llegar a api-reservas |
| Visibilidad (Hubble) | Funcionó. Sin ella, este incidente sería invisible |
| Alertas | Funcionó. La alerta saltó y por eso estamos investigando |
| Prevención del compromiso inicial | Falló. Algo permitió ejecutar código en el worker |
| Política de red del worker | Demasiado permisiva. No debería poder llegar a api-reservas |
| Autorización L7 | Ausente. Con acceso a api-reservas:8080, puede llamar a cualquier ruta |
| Cifrado interno | Ausente. El tráfico a postgres va en claro |
3. Acciones inmediatas, por prioridad
| # | Acción | Por qué |
|---|---|---|
| 1 | Aislar el pod sin destruirlo: cambiar su etiqueta app para que los Services y las políticas dejen de aplicarle, dejándolo corriendo |
Corta el acceso conservando la memoria y el estado para el análisis. Borrarlo destruye las pruebas |
| 2 | Aplicar una política que le deniegue todo, incluida la base de datos | Detiene el acceso a datos personales en curso |
| 3 | Rotar las credenciales de postgres-reservas y revisar los pg_stat y logs de la base de datos |
El proceso tuvo acceso legítimo; hay que saber qué consultó |
| 4 | Consultar el registro de auditoría del apiserver (08-06): ¿usó la ServiceAccount? ¿leyó Secrets? | Determinar el alcance real |
| 5 | Revisar los flujos de las últimas 72 horas de ese pod y de todos los del namespace | Establecer cuándo empezó y si hay otros pods afectados |
| 6 | Notificar al responsable de cumplimiento normativo | Hay indicios de acceso no autorizado a datos personales. Los plazos de notificación empiezan a correr desde el conocimiento del hecho |
| 7 | Escanear la imagen del worker en busca de vulnerabilidades conocidas (08-06) | Encontrar el vector de entrada |
| 8 | Endurecer la política del worker: quitar el acceso a api-reservas |
Corregir el permiso de más |
| 9 | Revisar el resto de políticas buscando permisos igual de amplios | El mismo error puede estar en otros sitios |
Las órdenes de los dos primeros pasos:
# 1. Aislar: quitarle la etiqueta que lo hace destinatario de políticas y Services
kubectl label pod -n rutas-norte-pro worker-notif-k9x2 app-
kubectl label pod -n rutas-norte-pro worker-notif-k9x2 estado=en-cuarentena
# 2. Denegación total para el pod en cuarentena
kubectl apply -f - <<'YAML'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: cuarentena
namespace: rutas-norte-pro
spec:
podSelector:
matchLabels:
estado: en-cuarentena
policyTypes: [Ingress, Egress]
# Sin reglas: se deniega absolutamente todo
YAMLAl quitar la etiqueta app, el ReplicaSet detecta que le falta una réplica y crea un pod nuevo y limpio: el servicio se recupera solo mientras el pod comprometido queda aislado para el análisis. Es una técnica que conviene tener ensayada antes de necesitarla.
4. Los escenarios contrafactuales
Sin control de salida: las tres conexiones a 192.0.2.55 habrían sido FORWARDED. El proceso tenía acceso legítimo a postgres-reservas, es decir, a la tabla de clientes con nombres, DNI, teléfonos y correos. Esto habría sido una fuga de datos personales consumada, con obligación de notificación a la autoridad de control y a los afectados. La política de egreso —esas líneas de YAML que casi nadie escribe— es lo único que separó un incidente contenido de una brecha de datos.
Sin Hubble: no habría alerta. Las conexiones denegadas son silenciosas: sin registro de flujos, el pod comprometido habría seguido consultando la base de datos indefinidamente, y probablemente habría encontrado antes o después una vía de salida (una configuración cambiada, un permiso nuevo, una política mal revisada). El incidente se habría descubierto semanas o meses después, o nunca.
Este ejercicio resume la lección entera: la prevención falló —siempre acaba fallando en algún punto—, la contención funcionó, y la detección permitió reaccionar. Las tres son necesarias, y la que más se descuida es la tercera.
Conclusión
Hemos construido la estrategia de red completa de Rutas Norte, por capas:
- La microsegmentación parte de denegar por defecto en los tres entornos, con políticas por componente y no por namespace, y con revisión periódica de qué conversaciones siguen siendo necesarias.
- El control del tráfico saliente es la capa que decide si un compromiso se convierte en una fuga de datos. Cuatro de los seis componentes de Rutas Norte no necesitan salir a internet en absoluto, y cerrarles la salida es la medida de mayor valor por menor coste de toda la lección.
- Las NetworkPolicies trabajan con IP, no con nombres de dominio. Las salidas son la pasarela de salida, el proxy con lista de dominios permitidos (que además registra los destinos) o las políticas
toFQDNsde Cilium. - Dentro del clúster, tras el TLS del Ingress, todo viaja en claro. Las opciones son TLS en la aplicación (prioritario para
postgres-reservas), cifrado a nivel de CNI con WireGuard (barato y efectivo entre nodos) o mTLS con una malla de servicio. - Una malla de servicio da identidad criptográfica por carga, mTLS automático y autorización L7, a cambio de complejidad operativa, recursos y latencia. Rutas Norte no la necesita hoy, y hemos fijado criterios objetivos para revisar esa decisión. Istio (sidecar o ambient), Linkerd y Cilium Service Mesh cubren perfiles distintos.
- En el perímetro: un único punto de entrada (nada de NodePort), WAF adoptado gradualmente, limitación de peticiones y cabeceras de seguridad.
- El plano de control merece revisión propia: apiserver no expuesto, etcd cifrado y con los Secrets reescritos, copias de seguridad cifradas, kubelet autenticado sin puerto de solo lectura, y nodos de control aislados con taints.
- Lo que le falta a NetworkPolicy es registro. Hubble, o el registro de un proxy de salida, convierte el sondeo interno y los intentos de exfiltración en algo visible y alertable.
- La confianza cero se resume en asumir el compromiso: no "cómo evito que entren", sino "cuando entren, qué pueden hacer y en cuánto me entero".
Con esto, la red de Rutas Norte está segmentada, con la salida controlada y con visibilidad de lo que ocurre. Pero fíjate en algo: todo lo que hemos hecho en este módulo protege el clúster en tiempo de ejecución. Damos por sentado que las imágenes que ejecutamos son las que creemos.
Y esa suposición es frágil. registry.rutasnorte.example/api-reservas:2.7.1 es una etiqueta, y una etiqueta se puede reasignar: la imagen que se descargó ayer puede no ser la de hoy. La imagen base sobre la que se construyó puede contener software que nadie ha revisado. Las dependencias que se instalaron durante la construcción vienen de repositorios públicos. Y nada, absolutamente nada, impide hoy que alguien con acceso al registro suba una imagen modificada con el mismo nombre y que el clúster la ejecute sin rechistar, con todos nuestros securityContext perfectamente aplicados a un contenedor que hace algo distinto de lo que creemos.
La siguiente lección, 08-05, Seguridad de Imágenes, recorre la cadena de suministro completa: dónde se puede envenenar una imagen, cómo construir imágenes mínimas, por qué el digest es la única referencia realmente reproducible, cómo firmar con Cosign y —lo más importante— cómo hacer que el clúster rechace cualquier imagen que no esté firmada, con la política de admisión que lo garantiza.
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
