Skip to content

Kubernetes Network Policies: Securing Pod Traffic

Master Kubernetes network policies to control pod-to-pod traffic, implement microsegmentation, and build defense-in-depth for your cluster workloads.

5 min read
Kubernetes cluster diagram showing network policies controlling traffic flow between pods

By default, every pod in a Kubernetes cluster can talk to every other pod. This flat network model is convenient for development but dangerous in production. A compromised pod can reach your database, internal APIs, and control plane without restriction.

Network policies are Kubernetes-native firewalls that control which pods can communicate. They implement microsegmentation—the practice of defining granular traffic rules between workloads rather than relying on perimeter security alone.

Understanding Default Network Behavior

Before any NetworkPolicy is applied, Kubernetes allows all ingress and egress traffic between pods. The moment you apply a policy to a pod, that pod switches from "allow all" to "deny by default" for the direction specified.

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

This single policy immediately locks down the entire namespace. No pod can receive or send traffic until you create allow rules. Start here and build up—it's much safer than starting open and trying to close gaps.

Building Allow Rules for Common Patterns

The most common pattern is a three-tier application: frontend talks to API, API talks to database. Each tier should only reach its immediate neighbor.

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

Now if the frontend pod is compromised, the attacker can reach the API but not the database directly. Each hop requires a separate compromise, dramatically increasing the difficulty of lateral movement.

Controlling Egress Traffic

Ingress policies protect pods from unwanted incoming connections. Egress policies prevent pods from making unwanted outgoing connections—equally important for preventing data exfiltration.

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

Always include DNS in egress rules. Without it, pods can't resolve any hostnames—including internal service names. This is the most common mistake when implementing egress policies.

Policy Testing and Validation

Network policies are notoriously hard to test. A misconfigured policy can break your application silently. Use dedicated testing tools and patterns.

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
}

Run these tests in CI against a staging cluster with the same policies applied. They serve as both documentation and regression tests—if someone modifies a policy, the test suite catches unintended changes.

Cross-Namespace Policies

Microservices often span multiple namespaces. Network policies support cross-namespace rules through namespace selectors, but the syntax requires careful attention.

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

Label your namespaces consistently. The name: monitoring label on the monitoring namespace is what makes the cross-namespace selector work. Without proper namespace labels, cross-namespace policies silently fail to match.

Debugging Connectivity Issues

When a network policy blocks traffic unexpectedly, debugging can be frustrating. A systematic approach saves hours of guesswork.

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;
}

Key Takeaways

Network policies transform Kubernetes security from a flat, open network into a properly segmented architecture. The most important principle is default-deny: start by blocking everything, then create explicit allow rules for known traffic patterns. This approach ensures that new deployments are secure by default rather than accidentally exposed.

The common pitfalls are predictable: forgetting DNS in egress rules, not labeling namespaces for cross-namespace policies, and deploying to a cluster whose CNI doesn't support policies. Test your policies in staging with automated connectivity probes, and treat them as code—versioned, reviewed, and deployed through CI alongside the workloads they protect.

Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX