🔒 Security Advanced ⏱️ 17 min

Kubernetes Network Policies for Zero Trust Security

Implement network segmentation and micro-segmentation in Kubernetes using Network Policies for defense in depth.

By Victor Robin

Introduction

In a flat network, a compromised pod can attack any other pod in the cluster. Network Policies act as a firewall for pods, enabling a Zero Trust architecture where communication is denied by default and explicitly allowed only where necessary. This guide demonstrates how to implement a defense-in-depth strategy using Kubernetes Network Policies, ensuring that services can only communicate with their valid dependencies.

Architecture Overview

Network Policies enforce a whitelist model. All traffic is blocked unless explicitly allowed.

flowchart TB
    Traefik["🌐 Ingress<br/>(Traefik)"] --> Web["💻 Web"]
    Web --> API["🛡️ API"]
    
    API --> DB[("🗄️ Database")]
    API --> Cache[("🧊 Redis")]
    
    subgraph Attack["🚫 Blocked Path"]
        Attacker["😈 Compromised<br/>Web Pod"] -.-> DB
    end

    classDef primary fill:#7c3aed,color:#fff
    classDef secondary fill:#06b6d4,color:#fff
    classDef db fill:#f43f5e,color:#fff
    classDef warning fill:#fbbf24,color:#000

    class Web,API primary
    class Traefik secondary
    class DB,Cache db
    class Attacker warning

Implementation

Default Deny Policies

Deny All Ingress

# core-infra/security/default-deny-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: archives-staging
spec:
  podSelector: {}
  policyTypes:
    - Ingress

Deny All Egress

# core-infra/security/default-deny-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: archives-staging
spec:
  podSelector: {}
  policyTypes:
    - Egress

Combined Default Deny

# core-infra/security/default-deny-all.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: archives-staging
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

Allow Policies

API to Database

# apps/archives-api/network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: archives-api-allow
  namespace: archives-staging
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: archives-api
  policyTypes:
    - Ingress
    - Egress
  ingress:
    # Allow from Traefik ingress
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: traefik-system
          podSelector:
            matchLabels:
              app.kubernetes.io/name: traefik
      ports:
        - protocol: TCP
          port: 8080
  egress:
    # Allow to PostgreSQL
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: data-layer
          podSelector:
            matchLabels:
              app.kubernetes.io/name: postgresql
      ports:
        - protocol: TCP
          port: 5432
    # Allow to NATS
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: data-layer
          podSelector:
            matchLabels:
              app.kubernetes.io/name: nats
      ports:
        - protocol: TCP
          port: 4222
    # Allow DNS
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Worker to Services

# apps/archives-workers/network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: archives-workers-allow
  namespace: archives-staging
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: archives-workers
  policyTypes:
    - Egress
  egress:
    # PostgreSQL
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: data-layer
          podSelector:
            matchLabels:
              app.kubernetes.io/name: postgresql
      ports:
        - protocol: TCP
          port: 5432
    # NATS
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: data-layer
          podSelector:
            matchLabels:
              app.kubernetes.io/name: nats
      ports:
        - protocol: TCP
          port: 4222
    # Qdrant
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: data-layer
          podSelector:
            matchLabels:
              app.kubernetes.io/name: qdrant
      ports:
        - protocol: TCP
          port: 6333
        - protocol: TCP
          port: 6334
    # MinIO
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: data-layer
          podSelector:
            matchLabels:
              app.kubernetes.io/name: minio
      ports:
        - protocol: TCP
          port: 9000
    # Ollama AI
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ai
          podSelector:
            matchLabels:
              app.kubernetes.io/name: ollama
      ports:
        - protocol: TCP
          port: 11434
    # DNS
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53

Web to API

# apps/bluerobin-web/network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: bluerobin-web-allow
  namespace: archives-staging
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: bluerobin-web
  policyTypes:
    - Ingress
    - Egress
  ingress:
    # Allow from Traefik
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: traefik-system
      ports:
        - protocol: TCP
          port: 8080
  egress:
    # Allow to API
    - to:
        - podSelector:
            matchLabels:
              app.kubernetes.io/name: archives-api
      ports:
        - protocol: TCP
          port: 8080
    # Allow to NATS (real-time updates)
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: data-layer
          podSelector:
            matchLabels:
              app.kubernetes.io/name: nats
      ports:
        - protocol: TCP
          port: 4222
    # DNS
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53

Database Layer Policies

PostgreSQL Access Control

# infrastructure/data-layer/postgresql-network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgresql-allow
  namespace: data-layer
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: postgresql
  policyTypes:
    - Ingress
  ingress:
    # Allow from staging API and workers
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: archives-staging
          podSelector:
            matchExpressions:
              - key: app.kubernetes.io/name
                operator: In
                values:
                  - archives-api
                  - archives-workers
                  - bluerobin-web
      ports:
        - protocol: TCP
          port: 5432
    # Allow from production
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: archives-prod
          podSelector:
            matchExpressions:
              - key: app.kubernetes.io/name
                operator: In
                values:
                  - archives-api
                  - archives-workers
                  - bluerobin-web
      ports:
        - protocol: TCP
          port: 5432
    # Allow from CNPG operator
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: cnpg-system
      ports:
        - protocol: TCP
          port: 5432

NATS Access Control

# infrastructure/data-layer/nats-network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: nats-allow
  namespace: data-layer
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: nats
  policyTypes:
    - Ingress
  ingress:
    # Client connections
    - from:
        - namespaceSelector:
            matchExpressions:
              - key: kubernetes.io/metadata.name
                operator: In
                values:
                  - archives-staging
                  - archives-prod
      ports:
        - protocol: TCP
          port: 4222
    # Cluster communication (between NATS pods)
    - from:
        - podSelector:
            matchLabels:
              app.kubernetes.io/name: nats
      ports:
        - protocol: TCP
          port: 6222
    # Leafnode connections
    - from:
        - podSelector:
            matchLabels:
              app.kubernetes.io/name: nats
      ports:
        - protocol: TCP
          port: 7422

CIDR-Based Rules

External API Access

# Allow egress to external APIs
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-external-apis
  namespace: archives-staging
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: archives-workers
  policyTypes:
    - Egress
  egress:
    # Allow HTTPS to external APIs
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - 10.0.0.0/8
              - 172.16.0.0/12
              - 192.168.0.0/16
      ports:
        - protocol: TCP
          port: 443

Cilium Enhanced Policies

Layer 7 HTTP Policy

# Cilium L7 policy for API path filtering
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: archives-api-l7
  namespace: archives-staging
spec:
  endpointSelector:
    matchLabels:
      app.kubernetes.io/name: archives-api
  ingress:
    - fromEndpoints:
        - matchLabels:
            app.kubernetes.io/name: bluerobin-web
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              - method: "GET"
                path: "/api/documents.*"
              - method: "POST"
                path: "/api/documents"
              - method: "GET"
                path: "/health.*"

DNS Policy

# Cilium DNS egress policy
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: allow-specific-dns
  namespace: archives-staging
spec:
  endpointSelector:
    matchLabels:
      app.kubernetes.io/name: archives-workers
  egress:
    - toEndpoints:
        - matchLabels:
            k8s:io.kubernetes.pod.namespace: kube-system
            k8s-app: kube-dns
      toPorts:
        - ports:
            - port: "53"
              protocol: UDP
          rules:
            dns:
              - matchPattern: "*.svc.cluster.local"
              - matchName: "api.openai.com"

Monitoring Network Policies

Policy Audit with Cilium

# Enable Hubble for network flow visibility
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: audit-deny
  namespace: archives-staging
  annotations:
    io.cilium.proxy-visibility: "<Egress/80/TCP/HTTP>,<Ingress/8080/TCP/HTTP>"
spec:
  endpointSelector: {}
  enableDefaultDeny:
    ingress: true
    egress: true

Validation Script

#!/bin/bash
# Test network policy enforcement

# Test allowed connection
kubectl run test-client --rm -i --restart=Never \
  --image=busybox \
  -n archives-staging \
  --labels="app.kubernetes.io/name=archives-api" \
  -- wget -qO- --timeout=5 \
  http://postgresql.data-layer.svc.cluster.local:5432 || echo "Connection blocked (expected)"

# Test denied connection
kubectl run test-client --rm -i --restart=Never \
  --image=busybox \
  -n archives-staging \
  --labels="app.kubernetes.io/name=test-pod" \
  -- wget -qO- --timeout=5 \
  http://postgresql.data-layer.svc.cluster.local:5432 && echo "Connection allowed (unexpected!)" || echo "Connection blocked (expected)"

Testing Strategy

Policy Test Matrix

SourceDestinationPortExpected
archives-apipostgresql5432Allow
archives-apinats4222Allow
archives-apiqdrant6333Deny
archives-workersqdrant6333Allow
bluerobin-webarchives-api8080Allow
bluerobin-webpostgresql5432Deny
traefikarchives-api8080Allow

Conclusion

Network Policies are a fundamental building block of Kubernetes security. By starting with a default-deny posture and whitelisting required traffic, you significantly reduce the attack surface of your cluster, turning it from a soft-shell target into a hardened fortress.

[Network Policies - Kubernetes Documentation] — Kubernetes