Cilium es un plugin de red (CNI) para Kubernetes que usa eBPF en el kernel de Linux para enrutar el tráfico de los pods, balancear servicios y aplicar políticas de red, incluidas reglas a nivel HTTP. En este tutorial crearás un clúster k3s en Ubuntu 24.04 sin su CNI ni kube-proxy de serie, instalarás Cilium en modo de sustitución de kube-proxy, aplicarás una política L7 y usarás Hubble para ver qué tráfico se permite y cuál se descarta.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS (kernel 6.8, más que suficiente para Cilium), por ejemplo un VPS de CubePath, con al menos 2 vCPU y 4 GB de RAM.
  • Un usuario no root con privilegios sudo.
  • La IP del servidor, que en la guía se indica como your_server_ip.

Si más adelante añades nodos, entre ellos deben estar abiertos 6443/TCP (API de Kubernetes), 10250/TCP (kubelet), 8472/UDP (túnel VXLAN de Cilium), 4240/TCP (health checks de Cilium) y 4244/TCP (Hubble).

Paso 1: Instalar k3s sin CNI ni kube-proxy

k3s trae Flannel y kube-proxy por defecto. Los desactivarás para que Cilium se encargue de la red de pods, de las políticas y del balanceo de servicios. Descarga el script de instalación oficial y revísalo antes de ejecutarlo:

curl -sfL https://get.k3s.io -o k3s-install.sh
less k3s-install.sh

Instala k3s con las opciones necesarias:

sudo INSTALL_K3S_EXEC="--flannel-backend=none --disable-network-policy --disable-kube-proxy" sh k3s-install.sh
  • --flannel-backend=none: no instala Flannel.
  • --disable-network-policy: desactiva el controlador de NetworkPolicy integrado, ya que Cilium las aplica.
  • --disable-kube-proxy: no ejecuta kube-proxy; Cilium lo sustituye.

Copia el kubeconfig a tu usuario para usar kubectl sin sudo:

mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown "$USER": ~/.kube/config
chmod 600 ~/.kube/config

Comprueba el nodo. Es normal que aparezca NotReady y que los pods de sistema estén en Pending, porque aún no hay CNI:

kubectl get nodes
NAME      STATUS     ROLES                  AGE   VERSION
cilium1   NotReady   control-plane,master   40s   v1.3x.x+k3s1

Paso 2: Instalar la CLI de Cilium

La CLI cilium instala Cilium en el clúster, muestra su estado y ejecuta pruebas de conectividad. Descarga la última versión estable junto con su suma de comprobación y verifícala:

CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
curl -L --fail --remote-name-all "https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz"{,.sha256sum}
sha256sum --check "cilium-linux-${CLI_ARCH}.tar.gz.sha256sum"
cilium-linux-amd64.tar.gz: OK

Instala el binario y limpia los archivos descargados:

sudo tar xzvfC "cilium-linux-${CLI_ARCH}.tar.gz" /usr/local/bin
rm "cilium-linux-${CLI_ARCH}.tar.gz"{,.sha256sum}
cilium version --client

Paso 3: Instalar Cilium en el clúster

Como no hay kube-proxy, los agentes de Cilium no pueden llegar a la API de Kubernetes a través del servicio kubernetes del clúster: hay que indicarles la IP y el puerto reales del API server. Además, k3s asigna a los pods el rango 10.42.0.0/16, así que lo configurarás como pool de IPs de Cilium:

cilium install \
  --set kubeProxyReplacement=true \
  --set k8sServiceHost=your_server_ip \
  --set k8sServicePort=6443 \
  --set ipam.operator.clusterPoolIPv4PodCIDRList=10.42.0.0/16

La CLI instala la versión de Cilium que corresponde a su versión estable. Espera a que todos los componentes estén listos:

cilium status --wait
Cilium:             OK
Operator:           OK
Envoy DaemonSet:    OK
Hubble Relay:       disabled
ClusterMesh:        disabled

DaemonSet              cilium             Desired: 1, Ready: 1/1, Available: 1/1
Deployment             cilium-operator    Desired: 1, Ready: 1/1, Available: 1/1

El nodo pasa ahora a Ready y los pods de sistema de k3s (CoreDNS, Traefik, metrics-server) arrancan:

kubectl get nodes
kubectl get pods -n kube-system

Comprueba que Cilium está sustituyendo a kube-proxy. Dentro del pod del agente, la herramienta de diagnóstico se llama cilium-dbg:

kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep KubeProxyReplacement
KubeProxyReplacement:    True   [eth0   your_server_ip fe80::... (Direct Routing)]

Paso 4: Validar la red con la prueba de conectividad

cilium connectivity test despliega pods de prueba y comprueba decenas de casos: tráfico entre pods, servicios, DNS, salida a Internet y políticas. Tarda varios minutos:

cilium connectivity test
[cilium-test-1] All 70 tests (600 actions) successful, 10 tests skipped, 0 scenarios skipped.

Algunas pruebas se omiten porque requieren varios nodos o funciones no activadas; lo importante es que no haya fallos. Al terminar, elimina el namespace de pruebas:

kubectl get ns | grep cilium-test
kubectl delete ns cilium-test-1

Paso 5: Activar Hubble

Hubble es la capa de observabilidad de Cilium: registra cada flujo de red con su origen, destino, protocolo y veredicto (permitido o descartado). Activa Hubble Relay y la interfaz web:

cilium hubble enable --ui
cilium status --wait

Instala la CLI de Hubble con el mismo procedimiento que la de Cilium:

HUBBLE_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/hubble/master/stable.txt)
HUBBLE_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then HUBBLE_ARCH=arm64; fi
curl -L --fail --remote-name-all "https://github.com/cilium/hubble/releases/download/${HUBBLE_VERSION}/hubble-linux-${HUBBLE_ARCH}.tar.gz"{,.sha256sum}
sha256sum --check "hubble-linux-${HUBBLE_ARCH}.tar.gz.sha256sum"
sudo tar xzvfC "hubble-linux-${HUBBLE_ARCH}.tar.gz" /usr/local/bin
rm "hubble-linux-${HUBBLE_ARCH}.tar.gz"{,.sha256sum}

Abre un port-forward hacia Hubble Relay en segundo plano y comprueba que responde:

cilium hubble port-forward &
hubble status
Healthcheck (via localhost:4245): Ok
Current/Max Flows: 4,095/4,095 (100.00%)
Flows/s: 12.34
Connected Nodes: 1/1

Paso 6: Aplicar una política de red L7

Despliega un servidor web y dos clientes: uno con la etiqueta app=client, que tendrá permiso, y otro sin ella:

kubectl create deployment web --image=nginx
kubectl expose deployment web --port=80
kubectl run client --image=curlimages/curl --labels=app=client --command -- sleep infinity
kubectl run intruder --image=curlimages/curl --command -- sleep infinity
kubectl wait --for=condition=Ready pod -l app=web --timeout=120s
kubectl wait --for=condition=Ready pod/client pod/intruder --timeout=120s

Crea una CiliumNetworkPolicy que solo permita a los pods app=client hacer peticiones GET al puerto 80 de web. Al seleccionar los pods de web, todo lo demás que les llegue queda bloqueado:

nano web-policy.yaml
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: web-allow-get-from-client
spec:
  endpointSelector:
    matchLabels:
      app: web
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: client
      toPorts:
        - ports:
            - port: "80"
              protocol: TCP
          rules:
            http:
              - method: GET
                path: "/.*"

Aplica la política:

kubectl apply -f web-policy.yaml

Comprueba los tres casos. Una petición GET desde client funciona:

kubectl exec client -- curl -s -o /dev/null -w '%{http_code}\n' http://web
200

Una petición POST desde el mismo pod la rechaza el proxy L7 de Cilium con un 403:

kubectl exec client -- curl -s -o /dev/null -w '%{http_code}\n' -X POST http://web
403

Y desde intruder la conexión ni siquiera se establece, así que curl agota el tiempo de espera:

kubectl exec intruder -- curl -s --max-time 5 http://web
command terminated with exit code 28

Paso 7: Observar el tráfico con Hubble

Consulta los flujos descartados de los últimos minutos. Verás los paquetes de intruder bloqueados por la política:

hubble observe --verdict DROPPED --last 20
Sep 25 12:10:03.114: default/intruder:40512 (ID:12345) <> default/web-5d8f9c7b6-x2k4p:80 (ID:23456) policy-verdict:none INGRESS DENIED (TCP Flags: SYN)
Sep 25 12:10:03.114: default/intruder:40512 (ID:12345) <> default/web-5d8f9c7b6-x2k4p:80 (ID:23456) Policy denied DROPPED (TCP Flags: SYN)

Y las peticiones HTTP que ha inspeccionado el proxy L7, incluido el POST denegado:

hubble observe --protocol http --last 20

Para la interfaz gráfica, abre un port-forward hacia Hubble UI; la CLI te indica la URL local (por defecto http://localhost:12000):

cilium hubble ui

Si el clúster está en un servidor remoto, crea antes un túnel SSH desde tu equipo con ssh -L 12000:localhost:12000 your_user@your_server_ip y abre esa URL en tu navegador.

Solución de problemas

Los pods de Cilium están en CrashLoopBackOff y los logs hablan de la API de Kubernetes. Casi siempre es un k8sServiceHost incorrecto. Revisa con kubectl -n kube-system logs ds/cilium y reinstala con la IP correcta: cilium uninstall y de nuevo cilium install.

Los pods reciben IPs fuera de 10.42.0.0/16 o hay problemas de enrutado. Comprueba el valor de ipam.operator.clusterPoolIPv4PodCIDRList con cilium config view | grep cluster-pool.

Una política bloquea tráfico que esperabas permitido. Usa hubble observe --verdict DROPPED --namespace <ns> para ver qué origen y destino se están descartando, y recuerda que en cuanto una política selecciona un pod, todo lo que no se permita explícitamente en esa dirección queda denegado, incluido DNS si es una política de salida.

Conclusión

Has creado un clúster k3s sin Flannel ni kube-proxy, instalado Cilium con sustitución de kube-proxy por eBPF, aplicado una política que filtra por método HTTP y verificado con Hubble qué tráfico se permite y cuál se descarta. Como siguientes pasos puedes añadir nodos worker al clúster, activar el cifrado transparente entre nodos con WireGuard (encryption.enabled=true y encryption.type=wireguard) y exportar las métricas de Cilium y Hubble a tu sistema de monitorización.