Saltar al contenido principal

Network Policies en Kubernetes

En el capítulo de seguridad vimos quién puede hablar con la API y con qué privilegios se ejecuta cada contenedor. Nos queda la red: quién puede hablar con quién dentro del clúster.

Por defecto, cualquier pod puede hablar con cualquier otro, esté en el namespace que esté. Una NetworkPolicy es como el portero de una discoteca: decide quién entra (Ingress) y adónde puede ir cada uno (Egress).

La regla de oro: en cuanto una política selecciona a un pod, todo el tráfico del tipo indicado queda denegado para ese pod, salvo lo que la política permita expresamente. Y las políticas se suman: si dos políticas seleccionan al mismo pod, vale lo que permita cualquiera de ellas.

Preparar el laboratorio​

Todos los ejercicios usan un namespace propio, así que al final basta con borrarlo:

kubectl create namespace red

Las NetworkPolicies necesitan un plugin de red (CNI) que las soporte, como Cilium o Calico. Con Flannel a secas se ignoran sin avisar: la política se crea, pero no bloquea nada.

Ejercicio 1: el portero de la web​

Prepara una web y dos clientes: uno con la etiqueta rol=cliente y otro sin ella.

kubectl -n red run web --image=nginx:alpine --labels=app=web --expose --port=80
kubectl -n red run cliente --image=busybox:1.36 --labels=rol=cliente -- sleep 3600
kubectl -n red run intruso --image=busybox:1.36 -- sleep 3600

Una forma rápida de probar los dos a la vez:

for p in cliente intruso; do
echo -n "$p -> web: "
kubectl -n red exec $p -- wget -qO- -T3 http://web > /dev/null 2>&1 && echo OK || echo BLOQUEADO
done
cliente -> web: OK
intruso -> web: OK

Paso 1. Cerrar la puerta a todo. podSelector: {} selecciona todos los pods del namespace, y sin reglas de ingress no se permite nada:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: red
spec:
podSelector: {}
policyTypes:
- Ingress

Aplícala y repite la prueba: los dos bloqueados.

Paso 2. Dejar pasar solo a los clientes:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: web-solo-clientes
namespace: red
spec:
podSelector:
matchLabels:
app: web
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
rol: cliente
ports:
- protocol: TCP
port: 80
cliente -> web: OK
intruso -> web: BLOQUEADO

Paso 3. El intruso se disfraza. ¿Qué pasa si alguien le pone la etiqueta?

kubectl -n red label pod intruso rol=cliente
cliente -> web: OK
intruso -> web: OK

Pasa. Las NetworkPolicies confían en las etiquetas, así que quien puede etiquetar pods puede saltárselas. Por eso los permisos sobre pods (RBAC) y las políticas de red van de la mano. Quítale la etiqueta antes de seguir:

kubectl -n red label pod intruso rol-

Ejercicio 2: cerrar la salida (y no romper el DNS)​

Deniega también todo el tráfico de salida del namespace:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: red
spec:
podSelector: {}
policyTypes:
- Egress

Prueba desde el cliente:

kubectl -n red exec cliente -- wget -qO- -T3 http://web
wget: bad address 'web'

Fíjate en el error: no es que la web no responda, es que ya ni siquiera resuelve el nombre. Al cerrar la salida, hemos cerrado también el camino hacia el DNS del clúster.

Pregunta: escribe una política que permita al cliente usar el DNS y llegar a la web, y nada más.

Solución
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: permitir-dns-y-web
namespace: red
spec:
podSelector:
matchLabels:
rol: cliente
policyTypes:
- Egress
egress:
- to: # el DNS del clúster
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to: # la web
- podSelector:
matchLabels:
app: web
ports:
- protocol: TCP
port: 80
cliente -> web: OK
intruso -> web: BLOQUEADO

Y hacia internet, el cliente sigue bloqueado: solo puede ir a donde hemos dicho.

La trampa del guion: AND frente a OR​

Mira con atención estas dos reglas. Solo cambia un guion:

# Regla A: UN elemento con dos selectores -> deben cumplirse LOS DOS (AND)
ingress:
- from:
- namespaceSelector:
matchLabels:
equipo: frontend
podSelector:
matchLabels:
rol: cliente
# Regla B: DOS elementos -> basta con cumplir UNO (OR)
ingress:
- from:
- namespaceSelector:
matchLabels:
equipo: frontend
- podSelector:
matchLabels:
rol: cliente
  • Regla A: pods con rol=cliente que estén en namespaces con equipo=frontend.
  • Regla B: cualquier pod de los namespaces equipo=frontend, o los pods con rol=cliente del namespace de la política.

Es uno de los errores más comunes, y una pregunta clásica de examen.

La otra trampa: el puerto es el del pod, no el del Service​

En una NetworkPolicy, ports se refiere al puerto en el que escucha el pod, no al del Service por el que lo llamamos. La política se evalúa cuando el tráfico ya ha pasado por el Service y va directo al pod.

En el ejercicio 1 coinciden (nginx escucha en el 80 y el Service publica el 80), pero no siempre es así. Con un http-echo, que escucha en el 5678 y publica un Service en el 80:

kubectl -n red create deployment eco --image=hashicorp/http-echo:1.0 -- /http-echo -text="hola"
kubectl -n red expose deployment eco --port=80 --target-port=5678

Una política de entrada para los pods app=eco con port: 80 bloquea las peticiones a http://eco, aunque el cliente esté llamando al 80. Lo mismo pasa en las reglas de egress del cliente: con la política del ejercicio 2, también ahí hay que abrir el puerto del pod. La regla correcta abre el 5678:

ports:
- protocol: TCP
port: 5678 # el targetPort del Service, donde escucha el contenedor

Si el contenedor declara un nombre en containerPort, también podemos usar ese nombre (port: http) y olvidarnos del número.

Un ejemplo completo​

Para terminar, una política que combina todo: entrada desde un rango de IPs (con una excepción), desde un namespace y desde unos pods concretos, y salida solo hacia un rango y un puerto.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ingress-egress-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- ipBlock:
cidr: 172.17.0.0/16
except:
- 172.17.1.0/24
- namespaceSelector:
matchLabels:
project: myproject
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 6379
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/24
ports:
- protocol: TCP
port: 5978

Con lo visto en la trampa del guion: los tres orígenes de from son elementos distintos, así que basta con cumplir uno.

Veremos patrones más avanzados en la especialización CKS.

Limpiar el laboratorio​

kubectl delete namespace red

Retos para practicar​

Sin solución: con lo visto en el capítulo, deberías poder resolverlos.

  1. En un namespace con default-deny de entrada y salida, haz que un pod cliente llegue a una web que está en otro namespace.
  2. Protege un Redis (puerto 6379) para que solo lo alcancen los pods con app=api de su mismo namespace. Comprueba que un pod sin esa etiqueta no conecta.

Resumen​

  • Sin NetworkPolicies, todo el tráfico está permitido. En cuanto una política selecciona un pod, solo pasa lo que se permita expresamente, y las políticas se suman.
  • Al cerrar la salida, acuérdate del DNS.
  • Vigila el guion: un elemento con dos selectores es AND; dos elementos son OR.
  • En ports va el puerto del pod, no el del Service.
  • Las políticas confían en las etiquetas: quien puede etiquetar pods puede saltárselas, así que van de la mano del RBAC.

En los próximos capítulos profundizaremos en usuarios y service accounts y en roles y permisos RBAC.


Volver al índice