Kubernetes Network Policies with Cilium: eBPF Microsegmentation Guide

Security tutorial - IT technology blog
Security tutorial - IT technology blog

Kubernetes ships with a flat network model by default. Every pod can talk directly to every other pod across namespaces without barriers. If an attacker breaches a public-facing container, nothing stops them from probing your internal Redis cache, payment database, or Kubelet endpoints.

Traditional CNI plugins enforce standard NetworkPolicy rules through iptables or IPVS. As clusters grow beyond 1,000 pods and tens of thousands of security rules, the Linux kernel must evaluate sequential packet filters linearly.

This creates noticeable connection latency—often jumping from sub-millisecond setup times to 5–15ms under heavy connection churn. Cilium solves this by replacing legacy packet filters with eBPF (Extended Berkeley Packet Filter), running compiled security programs directly inside kernel network hooks.

Quick Start: Install Cilium and Enforce Default-Deny

You can establish a solid zero-trust baseline in five minutes using the Cilium CLI.

Step 1: Install Cilium CLI and Deploy the Datapath

# Download and install the latest Cilium CLI
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"
sudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/bin
rm cilium-linux-${CLI_ARCH}.tar.gz

# Deploy Cilium into the cluster
cilium install --version 1.15.4

# Wait for agent pods and eBPF maps to initialize
cilium status --wait

Step 2: Deploy Demo Workloads

Create two simple pods in a dedicated test namespace: a frontend web server and a backend Redis cache.

kubectl create namespace demo-secure

# Deploy frontend
kubectl run frontend --namespace=demo-secure \
  --image=nginx:alpine --labels="app=frontend,tier=public"

# Deploy backend cache
kubectl run backend --namespace=demo-secure \
  --image=redis:7-alpine --labels="app=backend,tier=secure"

Step 3: Apply a Default-Deny Policy

Lock down the namespace completely. Applying an empty endpoint selector without ingress or egress rules immediately isolates all pods in demo-secure from unexpected traffic.

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: default-deny-all
  namespace: demo-secure
spec:
  endpointSelector:
    matchLabels: {}

Apply the isolation manifest:

kubectl apply -f default-deny.yaml

Any new TCP handshake between frontend and backend now drops immediately at the socket layer before consuming network stack resources.

Deep Dive: How eBPF Reinvents Microsegmentation

Traditional Linux filtering routes every packet through netfilter chains (PREROUTING, FORWARD, POSTROUTING). When a cluster maintains 20,000 iptables rules, evaluating them one-by-one consumes significant CPU cycles and creates packet processing jitter.

Cilium bypasses netfilter entirely by attaching eBPF bytecode programs to low-level hooks like Traffic Control (tc), XDP, and cgroup/sock_ops. This architecture delivers three core advantages:

  • Identity-based security: Instead of tracking shifting pod IP addresses, Cilium assigns a numeric Security Identity (e.g., ID: 4120) derived from Kubernetes label sets.
  • O(1) Map Lookups: When a packet arrives, eBPF queries a kernel BPF hash map in constant time. The lookup takes roughly 50 nanoseconds regardless of whether you have 10 rules or 50,000 rules.
  • Socket-level bypass (sockmap): For pods communicating on the same worker node, Cilium streams data directly between Unix sockets, shaving 15–20% off round-trip latency by bypassing TCP/IP serialization entirely.

Here is how to craft a precise CiliumNetworkPolicy (CNP) that permits only the frontend pod to query Redis on port 6379:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: demo-secure
spec:
  endpointSelector:
    matchLabels:
      app: backend
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: frontend
    toPorts:
    - ports:
      - port: "6379"
        protocol: TCP

Verify connectivity from the frontend container:

# Test connection (should return successful open status)
BACKEND_IP=$(kubectl get pod backend -n demo-secure -o jsonpath='{.status.podIP}')
kubectl exec -n demo-secure frontend -- nc -zv -w 2 $BACKEND_IP 6379

Advanced Filtering: Layer 7 HTTP Paths and DNS-Aware Egress

Standard L3/L4 policies only regulate IP addresses and ports. Modern microservices, however, demand fine-grained Layer 7 controls. Cilium embeds an Envoy proxy instance directly into the local agent, inspecting HTTP methods and headers without requiring sidecar injection into application pods.

Scenario 1: Restricting HTTP Methods and REST Endpoints

Consider an analytics collector that only needs to query GET /metrics from a billing service. You can explicitly block dangerous operations like POST /charge or DELETE /accounts directly at the CNI layer.

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: restrict-billing-api
  namespace: demo-secure
spec:
  endpointSelector:
    matchLabels:
      app: billing-service
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: analytics-collector
    toPorts:
    - ports:
      - port: "8080"
        protocol: TCP
      rules:
        http:
        - method: "GET"
          path: "/metrics"

Scenario 2: DNS-Aware Egress Lockdown

Attackers who compromise a container often download payloads or exfiltrate credentials to arbitrary external servers. Cilium inspects outbound DNS queries and dynamically opens egress pinholes only for approved FQDN targets.

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: lock-egress-to-stripe
  namespace: demo-secure
spec:
  endpointSelector:
    matchLabels:
      app: frontend
  egress:
  # Permit CoreDNS lookups
  - toEndpoints:
    - matchLabels:
        "k8s:io.kubernetes.pod.namespace": kube-system
        "k8s:k8s-app": kube-dns
    toPorts:
    - ports:
      - port: "53"
        protocol: ANY
      rules:
        dns:
        - matchPattern: "*"
  # Allow outbound HTTPS strictly to Stripe API
  - toFQDNs:
    - matchName: "api.stripe.com"
    toPorts:
    - ports:
      - port: "443"
        protocol: TCP

Production Hardening and Troubleshooting Tips

Rolling out microsegmentation in active clusters requires careful rollout strategies to prevent unintentional downtime.

  • Audit before enforcing: Annotate your policies with io.cilium.audit-mode: "true". This logs policy matches to Hubble without dropping packets, letting you spot missing rules before turning on hard enforcement.
  • Trace drops in real time with Hubble: Use Cilium’s observability engine to watch packet drops as they happen:
    # Stream dropped network flows in real time
    hubble observe --namespace demo-secure --verdict DROPPED --follow
  • Block cloud metadata endpoints globally: Deploy a CiliumClusterwideNetworkPolicy (CCNP) to block pods across every namespace from reaching cloud instance metadata services (169.254.169.254/32), protecting against SSRF attacks.
  • Enforce strong administrative credentials: Securing network layers is only half the battle; administrative access to nodes and cluster bastions needs equal diligence. For generating secure random operational passwords, the browser-based generator at toolcraft.app/en/tools/security/password-generator provides a quick, zero-network-exposure utility.

Switching from legacy packet filters to eBPF gives you precise Layer 3 through Layer 7 policy enforcement with consistent O(1) performance. It turns zero-trust Kubernetes networking from an operational bottleneck into a scalable security foundation.

Share: