Por defecto, en Kubernetes cualquier pod puede conectarse a cualquier otro pod del clúster y a Internet. Las NetworkPolicies permiten restringir ese tráfico a nivel de pod, seleccionando por etiquetas qué conexiones de entrada (ingress) y de salida (egress) están permitidas. En este tutorial desplegarás una pequeña aplicación de prueba, aplicarás una política de denegación por defecto y abrirás solo el tráfico necesario, comprobando cada regla con pruebas de conexión reales.
Requisitos previos
Para seguir esta guía necesitas:
- Un clúster de Kubernetes con un plugin de red (CNI) que aplique NetworkPolicies, como Calico, Cilium o el controlador integrado de k3s. Flannel por sí solo no las aplica: las políticas se crean sin error pero no tienen ningún efecto.
kubectlconfigurado con permisos para crear namespaces, pods y NetworkPolicies, desde una máquina con Ubuntu 24.04 o cualquier otro sistema.
Comprueba qué CNI usa tu clúster mirando los pods de kube-system:
kubectl get pods -n kube-system -o wide | grep -Ei 'calico|cilium|flannel|kube-router'
Si ves pods de calico-node o cilium, el clúster aplica políticas. Si solo ves kube-flannel, instala Calico o Cilium antes de continuar.
Cómo funcionan las NetworkPolicies
Tres reglas explican casi todo su comportamiento:
- Un pod que no está seleccionado por ninguna política acepta todo el tráfico.
- En cuanto una política con
policyTypes: [Ingress]selecciona un pod, ese pod solo acepta el tráfico de entrada que alguna política permita explícitamente. Lo mismo ocurre conEgresspara el tráfico de salida. - Las políticas son aditivas: no hay reglas de denegación, y si varias políticas seleccionan un pod, se suma todo lo que permiten.
Las respuestas de una conexión permitida siempre se aceptan, así que no necesitas reglas para el tráfico de retorno. Los orígenes y destinos se definen con podSelector, namespaceSelector o ipBlock (rangos CIDR).
Paso 1: Desplegar la aplicación de prueba
Crea un namespace demo con dos componentes: un backend (Nginx expuesto con un Service) y un frontend que será el único autorizado a hablar con él. Crea también un pod intruso para comprobar que el resto del tráfico se bloquea:
kubectl create namespace demo
kubectl -n demo create deployment backend --image=nginx:1.29 --port=80
kubectl -n demo expose deployment backend --port=80
kubectl -n demo run frontend --image=busybox:1.37 --labels=app=frontend -- sleep 1d
kubectl -n demo run intruso --image=busybox:1.37 --labels=app=intruso -- sleep 1d
kubectl create deployment etiqueta automáticamente los pods con app=backend. Espera a que todo esté listo:
kubectl -n demo get pods --show-labels
NAME READY STATUS RESTARTS AGE LABELS
backend-7c9f8d6b5d-q2x7m 1/1 Running 0 40s app=backend,pod-template-hash=7c9f8d6b5d
frontend 1/1 Running 0 35s app=frontend
intruso 1/1 Running 0 33s app=intruso
Comprueba que, sin políticas, ambos pods llegan al backend:
kubectl -n demo exec frontend -- wget -qO- -T 3 http://backend | grep -o '<title>.*</title>'
kubectl -n demo exec intruso -- wget -qO- -T 3 http://backend | grep -o '<title>.*</title>'
<title>Welcome to nginx!</title>
<title>Welcome to nginx!</title>
Paso 2: Denegar todo el tráfico de entrada por defecto
La base de un modelo de mínimo privilegio es una política que selecciona todos los pods del namespace (podSelector: {}) y no permite ninguna entrada:
nano default-deny-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: demo
spec:
podSelector: {}
policyTypes:
- Ingress
Aplícala y repite la prueba desde el frontend:
kubectl apply -f default-deny-ingress.yaml
kubectl -n demo exec frontend -- wget -qO- -T 3 http://backend
wget: download timed out
command terminated with exit code 1
Ahora ningún pod del namespace acepta conexiones entrantes.
Paso 3: Permitir solo el tráfico del frontend al backend
Crea una política que seleccione los pods app=backend y acepte conexiones al puerto 80 solo desde pods app=frontend del mismo namespace:
nano allow-frontend-to-backend.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: demo
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 80
El puerto indicado es el del contenedor (targetPort), no el del Service. Aplícala y comprueba ambos orígenes:
kubectl apply -f allow-frontend-to-backend.yaml
kubectl -n demo exec frontend -- wget -qO- -T 3 http://backend | grep -o '<title>.*</title>'
kubectl -n demo exec intruso -- wget -qO- -T 3 http://backend
<title>Welcome to nginx!</title>
wget: download timed out
command terminated with exit code 1
El frontend vuelve a tener acceso y el intruso sigue bloqueado.
Paso 4: Permitir tráfico desde otro namespace
Es habitual que un controlador de Ingress o una herramienta de monitorización de otro namespace necesite llegar a tus pods. Para seleccionar un namespace usa la etiqueta kubernetes.io/metadata.name, que Kubernetes añade automáticamente a todos los namespaces con su nombre. Crea un namespace monitoring con un pod de prueba:
kubectl create namespace monitoring
kubectl -n monitoring run probe --image=busybox:1.37 --labels=app=probe -- sleep 1d
Añade una política que permita a cualquier pod de monitoring conectar con el backend:
nano allow-monitoring.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-monitoring
namespace: demo
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- protocol: TCP
port: 80
kubectl apply -f allow-monitoring.yaml
kubectl -n monitoring exec probe -- wget -qO- -T 3 http://backend.demo.svc.cluster.local | grep -o '<title>.*</title>'
<title>Welcome to nginx!</title>
Importantela indentación cambia el significado. Si
namespaceSelectorypodSelectorvan en el mismo elemento de la listafrom(sin un guion delante del segundo), se exigen ambas condiciones: pods con esa etiqueta dentro de ese namespace. Si cada uno lleva su propio guion, son dos orígenes alternativos y basta con cumplir uno.
Paso 5: Restringir el tráfico de salida
Limitar la salida impide que un pod comprometido se conecte a otros servicios internos o descargue herramientas de Internet. Aplica una denegación de salida por defecto a todo el namespace, pero permite las consultas DNS a CoreDNS; sin ellas, ningún pod podría resolver nombres de Service:
nano default-deny-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: demo
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
CoreDNS lleva la etiqueta k8s-app=kube-dns en kubeadm, k3s y la mayoría de distribuciones; compruébalo con kubectl -n kube-system get pods --show-labels | grep dns. Después, permite de forma explícita la salida del frontend hacia el backend:
nano allow-frontend-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-egress
namespace: demo
spec:
podSelector:
matchLabels:
app: frontend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 80
Aplica ambas y comprueba que el frontend sigue llegando al backend, pero ya no puede salir a Internet:
kubectl apply -f default-deny-egress.yaml -f allow-frontend-egress.yaml
kubectl -n demo exec frontend -- wget -qO- -T 3 http://backend | grep -o '<title>.*</title>'
kubectl -n demo exec frontend -- wget -qO- -T 3 http://example.com
<title>Welcome to nginx!</title>
wget: download timed out
command terminated with exit code 1
Si un pod necesita llegar a un servicio externo concreto, añade una regla de salida con ipBlock y su rango, por ejemplo cidr: 203.0.113.0/24, y el puerto correspondiente.
Paso 6: Revisar las políticas aplicadas
Lista las políticas del namespace y consulta el detalle de una de ellas:
kubectl -n demo get networkpolicy
kubectl -n demo describe networkpolicy allow-frontend-to-backend
NAME POD-SELECTOR AGE
allow-frontend-egress app=frontend 2m
allow-frontend-to-backend app=backend 9m
allow-monitoring app=backend 6m
default-deny-egress <none> 2m
default-deny-ingress <none> 11m
<none> en POD-SELECTOR significa que la política selecciona todos los pods del namespace.
Funciones extra de Calico y Cilium
La API estándar NetworkPolicy funciona igual con cualquier CNI compatible y es la que conviene usar por defecto. Calico y Cilium añaden sus propios recursos para casos que la API estándar no cubre:
- Calico ofrece
GlobalNetworkPolicy(políticas para todo el clúster, con orden de evaluación y reglas de denegación explícitas), que se gestionan concalicoctlo con el API server de Calico. - Cilium ofrece
CiliumNetworkPolicy, con reglas de capa 7 (métodos y rutas HTTP) y filtrado de salida por nombre de dominio.
Por ejemplo, con Cilium puedes permitir que el frontend solo salga a api.github.com por HTTPS. La primera regla permite el DNS a través del proxy de Cilium, que es lo que le permite conocer las IPs de ese nombre:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: frontend-to-github
namespace: demo
spec:
endpointSelector:
matchLabels:
app: frontend
egress:
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
k8s:k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: ANY
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: api.github.com
toPorts:
- ports:
- port: "443"
protocol: TCP
Este recurso solo existe en clústeres con Cilium; en cualquier otro CNI, kubectl apply fallará porque el tipo no está definido.
Solución de problemas
La política se crea pero no bloquea nada. El CNI no aplica NetworkPolicies (caso típico de Flannel). Comprueba el CNI como en los requisitos previos.
Tras la denegación de salida, los pods no resuelven nombres. La regla de DNS no coincide con los pods de CoreDNS. Revisa sus etiquetas y namespace con kubectl -n kube-system get pods --show-labels y ajusta el selector.
Un origen permitido sigue bloqueado. Comprueba que las etiquetas del pod coinciden exactamente con kubectl get pods --show-labels, que el puerto de la regla es el del contenedor y no el del Service, y que, si limitaste la salida, el pod de origen también tiene una regla de egress hacia el destino. Una conexión necesita estar permitida en la salida del origen y en la entrada del destino.
Las sondas de salud fallan después de aplicar la denegación de entrada. En la mayoría de CNI el tráfico del kubelet del propio nodo está permitido, pero no en todos. Si tus sondas empiezan a fallar, consulta la documentación de tu CNI sobre el tráfico desde el host.
Para limpiar el entorno de pruebas:
kubectl delete namespace demo monitoring
Conclusión
Has aplicado una denegación por defecto de entrada y salida en un namespace, permitido solo las conexiones necesarias entre pods y desde otro namespace, y comprobado cada regla con pruebas reales. Como siguientes pasos puedes añadir una política default-deny-ingress a cada namespace de aplicación como parte de su plantilla, combinar estas políticas con RBAC para limitar quién puede modificarlas, o usar las herramientas de observabilidad de tu CNI (como Hubble en Cilium) para ver qué flujos se permiten y cuáles se descartan.
