Limitar la comunicación entre tus pods en Kubernetes con Network Policies

Por defecto, cuando trabajas con contenedores en Kubernetes todos los pods que despliegas en tu clúster tienen la posibilidad de comunicarse entre ellos sin ningún tipo de restricción. Esto es así por diseño, pero no siempre es lo que deseamos, ya que es posible que tengamos más de un proyecto/aplicación en nuestro clúster que no tienen por qué comunicarse entre sí, además de que a nivel de seguridad esto puede suponer un riesgo importante. Es por ello que tenemos el recurso llamado Network Policies a nuestra disposición y hoy me gustaría compartir contigo un ejemplo que te ayude a empezar con ello.

Aplicación de ejemplo

Para que lo veas con un ejemplo, voy a apoyarme en una aplicación que tengo para mis pruebas, basada en el tutorial de Angular con la aplicación Tour Of Heroes, la cual he tuneado un poco con las siguientes piezas:

Aplicación de ejemplo para mostrar las Network Policies de Kubernetes

Como ves, la misma consta de tres componetes: un frontal desarrollado en Flask, una API en .NET Core y una base de datos con SQL Server.

Si no utilizas Network Policies, todas esta piezas por defecto pueden hablar entre sí sin problemas. Es decir, el frontal web puede hacer llamadas a cualquier puerto de la API y de la base de datos, la API del frontal y de la base de datos y esta última de las otras dos sin restricciones, por no mencionar con otras aplicaciones que pudieran existir dentro del clúster. Sin embargo, lo que me gustaría es que la misma solo pueda establecer comunicaciones en este sentido:

Restringir las comunicaciones entre pods con Network Policies en k8s

Como ves, solo hay un camino para que el frontal sea capaz de hablar con la base de datos, que es la API, esta solamente es capaz de hablar con la base de datos y esta última solo recibe llamadas de la API. Por otro lado, cualquier otra aplicación en el clúster, en otros namespaces, no sería capaz de comunicarse con estos componentes.

Clúster de ejemplo

Para poder probar este escenario, lo primero que necesitas es un clúster de Kubernetes. Además, este debe soportar alguno de los plugins de red que soporten Network Policies, ya que no en todos está disponible. En mi caso voy a utilizar Azure Kubernetes Service y el plugin de Azure para mostrarte cómo funciona:

# Variables
RESOURCE_GROUP="aks-network-policies"
LOCATION="westeurope"
AKS_NAME="aks-network-policies"
AKS_VNET="aks-vnet"
AKS_SUBNET="aks-subnet"
# Create resource group
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION
# Create VNET
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $AKS_VNET \
--address-prefixes 10.0.0.0/8 \
--subnet-name $AKS_SUBNET \
--subnet-prefixes 10.240.0.0/24
# Create a service principal and read in the application ID
SP=$(az ad sp create-for-rbac --output json)
SP_ID=$(echo $SP | jq -r .appId)
SP_PASSWORD=$(echo $SP | jq -r .password)
# Wait 15 seconds to make sure that service principal has propagated
echo "Waiting for service principal to propagate..."
sleep 15
# Get the virtual network resource ID
VNET_ID=$(az network vnet show --resource-group $RESOURCE_GROUP --name $AKS_VNET --query id -o tsv)
# Assign the service principal Contributor permissions to the virtual network resource
az role assignment create --assignee $SP_ID --scope $VNET_ID --role Contributor
# Get the virtual network subnet resource ID
SUBNET_ID=$(az network vnet subnet show --resource-group $RESOURCE_GROUP --vnet-name $AKS_VNET --name $AKS_SUBNET --query id -o tsv)
# Create AKS cluster
az aks create \
    --resource-group $RESOURCE_GROUP \
    --name $AKS_NAME \
    --generate-ssh-keys \
    --docker-bridge-address 172.17.0.1/16 \
    --dns-service-ip 10.2.0.10 \
    --service-cidr 10.2.0.0/24 \
    --vnet-subnet-id $SUBNET_ID \
    --service-principal $SP_ID \
    --client-secret $SP_PASSWORD \
    --network-plugin azure \
    --network-policy azure
# Get AKS cluster credentials
az aks get-credentials --resource-group $RESOURCE_GROUP --name $AKS_NAME

Esta configuración es la misma que puedes encontrar en la documentación oficial.

Desplegar la aplicación de ejemplo

Lo siguiente que necesitas es una aplicación de ejemplo con la que probar. La mía puedes encontrarla en este repositorio de GitHub y si te descargas el mismo puedes desplegarla de manera sencilla con el siguiente comando:

# Create tour of heroes app
k create ns tour-of-heroes 
k apply -f tour-of-heroes -n tour-of-heroes --recursive
watch kubectl get pod -n tour-of-heroes
k get svc -n tour-of-heroes

En el mismo repositorio tienes un archivo llamado client.http que, gracias a la extensión REST Client de Visual Studio Code, te puede ayudar a rellenar la base de datos a través de la API, para que la aplicación se vea de la siguiente forma:

Tour of heroes desplegada con héroes

Aplicar Network Policies sobre la aplicación

Ahora que ya tenemos todo listo, vamos a desplegar dos políticas: La primera de ellas nos va a permitir que solo la API pueda hablar con la base de datos a través del puerto 1433:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-call-sql
spec:
  podSelector:
    matchLabels:
      app: tour-of-heroes-sql
      role: db
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: tour-of-heroes-api
              role: backend
      ports:
        - protocol: TCP
          port: 1433

Como ves, se trata de un recurso del tipo Network Policy que tiene, en su apartado spec, una propiedad llamada podSelector para decir cuál es el pod/pods objetivo de esta política. Después existen dos tipos: Ingress y Egress. En este ejemplo estamos configurando el tipo Ingress, que suele ser el más común, que es para indicar de quién acepta comunicación entrante. En este caso, sería de todos aquellos pods en el namespace que tengan las etiquetas app: tour-of-heroes-api y role: backend (es decir, nuestra API), pero solo para el puerto 1433, que es por el que se hace la comunicación con la base de datos. El resto de puertos no serían accesibles por la API.

La segunda nos permitirá que solo el frontal web pueda hablar con la API:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-call-api
spec:
  podSelector:
    matchLabels:
      app: tour-of-heroes-api
      role: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: tour-of-heroes-web
              role: frontend
      ports:
        - protocol: TCP
          port: 5000

Es importante que tengas en cuenta que en este caso el puerto al que nos referimos no es al del objeto Service asociado a la API, que escucha por el puerto 80 y enruta al 5000, sino al puerto por el que realmente está escuchando el pod, que en este caso es el 5000.

Para aplicar ambas, si has clonado el repositorio, puedes hacerlo de forma sencilla a través de este comando:

# Apply network policies
kubectl apply -f network-policies/ -n tour-of-heroes

Otro dato importante es que necesitas que las mismas estén desplegadas en el mismo namespace donde se encuentra la aplicación, ya que de lo contrario no funcionarán correctamente.

Dicho esto, imagina que quisieras que otros pods en otro namespace pudieran acceder a uno de los componentes de este, como por ejemplo otro frontal de Flask apuntando a la API:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: tour-of-heroes-web
    role: frontend
  name: tour-of-heroes-web
spec:
  replicas: 1
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      app: tour-of-heroes-web
      role: frontend
  template:
    metadata:
      labels:
        app: tour-of-heroes-web
        role: frontend
    spec:
      containers:
        - image: ghcr.io/0gis0/tour-of-heroes-flask/tour-of-heroes-flask:49c7562
          name: tour-of-heroes-web
          ports:
            - containerPort: 5002
              name: web
          env:
            - name: API_URL
              value: http://tour-of-heroes-api.tour-of-heroes/api/hero

Nota: Fijate que en este caso la API_URL apunta al Service de la API del otro namespace: tour-of-heroes-api.tour-of-heroes.

¿Cómo lo resolveríamos? En ese caso, podríamos añadir, por ejemplo a allow-web-call-api, otro selector en el apartado from llamado namespaceSelector donde las etiquetas coincidan con las del namespace donde están esos pods:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-call-api
spec:
  podSelector:
    matchLabels:
      app: tour-of-heroes-api
      role: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: tour-of-heroes-web
              role: frontend
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: tour-of-heroes-two
      ports:
        - protocol: TCP
          port: 5000

Si además quisiéramos restringir el acceso solo a algunos pods en concreto dentro de este otro namespace podríamos combinar namespaceSelector con podSelector dentro de la misma regla:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-call-api
spec:
  podSelector:
    matchLabels:
      app: tour-of-heroes-api
      role: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: tour-of-heroes-web
              role: frontend
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: tour-of-heroes-two
          podSelector:
            matchLabels:
              app: tour-of-heroes-web
              role: frontend
      ports:
        - protocol: TCP
          port: 5000

Para probar que esto funciona correctamente, puedes desplegar el manifiesto anterior con este comando:

# Create a new front-end in a different namespace pointing to the same service
k create ns tour-of-heroes-two
k apply -f tour-of-heroes/frontend-in-a-different-ns -n tour-of-heroes-two
watch kubectl get pods -n tour-of-heroes-two
k get svc -n tour-of-heroes-two

Y probar con otro pod que las comunicaciones funcionan como se esperan:

# Execute alpine in a pod for testing
kubectl run test-$RANDOM --rm -i --tty --image=alpine -n tour-of-heroes-two -- sh
wget -qO- --timeout=2 http://tour-of-heroes-web # Success
wget -qO- --timeout=2 http://tour-of-heroes-api.tour-of-heroes # Timeout

¡Saludos!