Saltar al contenido

Políticas de red de Kubernetes: proteger el tráfico entre pods

Domina las políticas de red de Kubernetes para controlar el tráfico entre pods, aplicar microsegmentación y crear una defensa en profundidad.

6 min de lectura
Diagrama de un clúster de Kubernetes que muestra las políticas de red controlando el flujo de tráfico entre pods

De forma predeterminada, todos los pods de un clúster de Kubernetes pueden comunicarse entre sí. Este modelo de red plano resulta cómodo durante el desarrollo, pero es peligroso en producción. Un pod comprometido puede acceder a tu base de datos, a las APIs internas y al plano de control sin ninguna restricción.

Las políticas de red son firewalls nativos de Kubernetes que controlan qué pods pueden comunicarse entre sí. Implementan la microsegmentación: la práctica de definir reglas de tráfico granulares entre cargas de trabajo en lugar de depender únicamente de la seguridad perimetral.

Cómo entender el comportamiento de red predeterminado

Antes de aplicar cualquier NetworkPolicy, Kubernetes permite todo el tráfico de Ingress y de Egress entre pods. En el momento en que aplicas una política a un pod, ese pod pasa de «permitir todo» a «denegar por defecto» en la dirección especificada.

ymlyaml
# ❌ No network policies: every pod can reach everything
# Pod A (frontend) → Pod B (api) ✓
# Pod A (frontend) → Pod C (database) ✓  ← This shouldn't happen
# Pod B (api) → Pod C (database) ✓
# Pod D (compromised) → Pod C (database) ✓  ← Dangerous
ymlyaml
# ✅ Default deny: block everything, then allow explicitly
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}  # Applies to ALL pods in namespace
  policyTypes:
    - Ingress
    - Egress

Esta única política bloquea de inmediato todo el namespace. Ningún pod puede recibir ni enviar tráfico hasta que crees reglas de permiso. Empieza por aquí y ve construyendo desde cero: es mucho más seguro que partir de una configuración abierta e intentar cerrar huecos después.

Cómo crear reglas de permiso para patrones comunes

El patrón más común es una aplicación de tres niveles: el frontend se comunica con la API, y la API se comunica con la base de datos. Cada nivel solo debería poder alcanzar a su vecino inmediato.

ymlyaml
# Allow frontend pods to receive traffic from ingress controller
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-to-frontend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: frontend
      tier: web
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: ingress-system
          podSelector:
            matchLabels:
              app: nginx-ingress
      ports:
        - protocol: TCP
          port: 3000
ymlyaml
# Allow API pods to receive traffic ONLY from frontend pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-api
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api-server
      tier: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
              tier: web
      ports:
        - protocol: TCP
          port: 8080
ymlyaml
# Allow database pods to receive traffic ONLY from API pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-database
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres
      tier: database
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: api-server
              tier: backend
      ports:
        - protocol: TCP
          port: 5432

Ahora, si el pod del frontend se ve comprometido, el atacante puede llegar a la API, pero no directamente a la base de datos. Cada salto requiere un compromiso adicional, lo que aumenta drásticamente la dificultad de cualquier movimiento lateral.

Control del tráfico de Egress

Las políticas de Ingress protegen a los pods de conexiones entrantes no deseadas. Las políticas de Egress evitan que los pods realicen conexiones salientes no deseadas, algo igual de importante para prevenir la exfiltración de datos.

ymlyaml
# ❌ Pod can reach any external IP (data exfiltration risk)
# A compromised pod could send data to attacker-controlled servers
 
# ✅ Restrict API server egress to known destinations
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-server-egress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api-server
  policyTypes:
    - Egress
  egress:
    # Allow DNS resolution
    - to:
        - namespaceSelector:
            matchLabels:
              name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    # Allow connection to database
    - to:
        - podSelector:
            matchLabels:
              app: postgres
              tier: database
      ports:
        - protocol: TCP
          port: 5432
    # Allow connection to Redis
    - to:
        - podSelector:
            matchLabels:
              app: redis
      ports:
        - protocol: TCP
          port: 6379
    # Allow connection to external payment API
    - to:
        - ipBlock:
            cidr: 203.0.113.0/24  # Payment provider IP range
      ports:
        - protocol: TCP
          port: 443

Incluye siempre la resolución DNS en tus reglas de Egress. Sin ella, los pods no podrán resolver ningún nombre de host, ni siquiera los nombres de servicios internos. Este es el error más común al implementar políticas de Egress.

Pruebas y validación de políticas

Las políticas de red son notoriamente difíciles de probar. Una política mal configurada puede romper tu aplicación de forma silenciosa. Usa herramientas y patrones de prueba dedicados.

tstypescript
// Policy validation script
interface PolicyTestCase {
  name: string;
  source: { namespace: string; labels: Record<string, string> };
  destination: { namespace: string; labels: Record<string, string>; port: number };
  expected: "allow" | "deny";
}
 
const testCases: PolicyTestCase[] = [
  {
    name: "Frontend can reach API",
    source: {
      namespace: "production",
      labels: { app: "frontend", tier: "web" },
    },
    destination: {
      namespace: "production",
      labels: { app: "api-server", tier: "backend" },
      port: 8080,
    },
    expected: "allow",
  },
  {
    name: "Frontend cannot reach database directly",
    source: {
      namespace: "production",
      labels: { app: "frontend", tier: "web" },
    },
    destination: {
      namespace: "production",
      labels: { app: "postgres", tier: "database" },
      port: 5432,
    },
    expected: "deny",
  },
  {
    name: "API can reach database",
    source: {
      namespace: "production",
      labels: { app: "api-server", tier: "backend" },
    },
    destination: {
      namespace: "production",
      labels: { app: "postgres", tier: "database" },
      port: 5432,
    },
    expected: "allow",
  },
  {
    name: "Random pod cannot reach database",
    source: {
      namespace: "production",
      labels: { app: "debug-pod" },
    },
    destination: {
      namespace: "production",
      labels: { app: "postgres", tier: "database" },
      port: 5432,
    },
    expected: "deny",
  },
];
 
async function runPolicyTests(
  tests: PolicyTestCase[]
): Promise<{ passed: number; failed: number; results: string[] }> {
  let passed = 0;
  let failed = 0;
  const results: string[] = [];
 
  for (const test of tests) {
    const actual = await probeConnection(
      test.source,
      test.destination
    );
 
    if (actual === test.expected) {
      passed++;
      results.push(`✅ ${test.name}: ${actual} (expected ${test.expected})`);
    } else {
      failed++;
      results.push(`❌ ${test.name}: ${actual} (expected ${test.expected})`);
    }
  }
 
  return { passed, failed, results };
}
 
async function probeConnection(
  source: PolicyTestCase["source"],
  destination: PolicyTestCase["destination"]
): Promise<"allow" | "deny"> {
  // In practice: kubectl run a temp pod with source labels,
  // attempt connection to destination, check exit code
  return "allow"; // Placeholder
}

Ejecuta estas pruebas en tu pipeline de CI contra un clúster de staging con las mismas políticas aplicadas. Funcionan tanto como documentación como pruebas de regresión: si alguien modifica una política, la suite de pruebas detecta los cambios no deseados.

Políticas entre namespaces

Los microservicios suelen repartirse en varios namespaces. Las políticas de red admiten reglas entre namespaces mediante selectores de namespace, pero la sintaxis exige mucha atención.

ymlyaml
# Allow monitoring namespace to scrape metrics from all production pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-monitoring-scrape
  namespace: production
spec:
  podSelector: {}  # All pods in production
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: monitoring
          podSelector:
            matchLabels:
              app: prometheus
      ports:
        - protocol: TCP
          port: 9090  # Metrics port
ymlyaml
# Allow production pods to send logs to logging namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-logging-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              name: logging
          podSelector:
            matchLabels:
              app: fluentd
      ports:
        - protocol: TCP
          port: 24224

Etiqueta tus namespaces de forma coherente. La etiqueta name: monitoring en el namespace de monitoreo es lo que permite que funcione el selector entre namespaces. Sin las etiquetas de namespace correctas, las políticas entre namespaces fallan en encontrar coincidencias de forma silenciosa.

Depuración de problemas de conectividad

Cuando una política de red bloquea tráfico de forma inesperada, depurar el problema puede ser frustrante. Un enfoque sistemático ahorra horas de conjeturas.

shbash
# Step 1: Verify which policies apply to the target pod
kubectl get networkpolicy -n production -o wide
 
# Step 2: Check pod labels match policy selectors
kubectl get pod api-server-abc123 -n production --show-labels
 
# Step 3: Test connectivity from source pod
kubectl exec -it frontend-xyz789 -n production -- \
  wget --timeout=3 -qO- http://api-server:8080/health
 
# Step 4: Check if CNI plugin supports network policies
# (Not all do! Flannel without Calico won't enforce policies)
kubectl get pods -n kube-system | grep -E 'calico|cilium|weave'
tstypescript
// Automated connectivity diagnostic
interface ConnectivityDiagnostic {
  sourcePopod: string;
  targetPod: string;
  port: number;
  policiesApplied: string[];
  cniSupportsPolicy: boolean;
  namespaceLabelsCorrect: boolean;
  podLabelsMatch: boolean;
  portInPolicy: boolean;
}
 
function diagnoseBlockedTraffic(
  diag: ConnectivityDiagnostic
): string[] {
  const suggestions: string[] = [];
 
  if (!diag.cniSupportsPolicy) {
    suggestions.push(
      "CNI plugin does not support NetworkPolicy. " +
      "Install Calico, Cilium, or another policy-aware CNI."
    );
  }
 
  if (diag.policiesApplied.length === 0) {
    suggestions.push(
      "No policies found for target pod. " +
      "Check if default-deny exists in this namespace."
    );
  }
 
  if (!diag.podLabelsMatch) {
    suggestions.push(
      "Source pod labels don't match any ingress 'from' selector. " +
      "Verify labels on both pods and in the policy."
    );
  }
 
  if (!diag.portInPolicy) {
    suggestions.push(
      `Port ${diag.port} is not listed in any matching policy's port list.`
    );
  }
 
  return suggestions;
}

Conclusiones clave

Las políticas de red transforman la seguridad de Kubernetes: pasan de una red plana y abierta a una arquitectura correctamente segmentada. El principio más importante es la denegación por defecto: empieza bloqueando todo y luego crea reglas de permiso explícitas para los patrones de tráfico conocidos. Este enfoque garantiza que los nuevos despliegues sean seguros por defecto en lugar de quedar expuestos por accidente.

Los errores más comunes son predecibles: olvidar el DNS en las reglas de Egress, no etiquetar los namespaces para las políticas entre namespaces, y desplegar en un clúster cuyo CNI no admite políticas. Prueba tus políticas en staging con sondas de conectividad automatizadas, y trátalas como código: versionadas, revisadas y desplegadas mediante CI junto con las cargas de trabajo que protegen.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX