Skip to content
Stribog

Networking

All writing

Cilium vs Calico: eBPF Zero Trust Without the Sidecar Tax

Cilium against Calico for zero-trust Kubernetes networking: identity-based policy, WireGuard node encryption and Hubble visibility, without the sidecar tax.

Stribog22 min readUpdated 6 Aug 2026

The service mesh market has been running a slow-motion rationalization for three years, and by early 2026 the verdict is legible: GKE Dataplane V2 is implemented using Cilium and enabled by default for every new Autopilot cluster, and Azure CNI Powered by Cilium is the default virtual network on every AKS Automatic cluster (an explicit, supported option on AKS Standard). When two major hyperscalers make Cilium the default in their managed offerings, that is not a trend — it is a consolidation event. Cilium graduated from the CNCF on 11 October 2023, and its Hubble observability layer has moved from optional add-on to operational baseline. The hyperscaler default matters not because hyperscalers are right about everything, but because they run enough clusters at enough scale to surface the failure modes that matter.

The Cilium versus Istio framing that has dominated conference talks since 2021 is, at this point, the wrong frame. It presupposes a binary choice between two tools that solve overlapping but distinct problems. The right question is harder and more interesting: what security guarantees does your threat model actually require at the network layer, and what operational complexity are you willing to carry permanently? Most teams installing Istio are solving a perceived compliance problem — "zero trust means mTLS, mTLS means Istio" — without examining whether Cilium's native WireGuard encryption and identity-based L3/L4/L7 policy enforcement already satisfies that requirement without the certificate management overhead, sidecar injection complexity, and control-plane blast radius. For the workloads where Istio's traffic management capabilities (circuit breaking, external authorization, mesh-native retries) genuinely add value, this article is honest: use Istio ambient mode. But know precisely what you are buying and what the realistic running cost is.

This article is a production-grade walkthrough of the Cilium networking and security model, written for platform engineers who are either deploying Cilium for the first time or hardening an existing installation toward a defensible zero-trust posture. It covers the eBPF kernel architecture with specific hook points, identity-based policy with real YAML including an L7 HTTP rule, WireGuard transparent encryption, Hubble flow observability, and an honest assessment of where Istio ambient mode still justifies its operational weight. The goal is a default-deny policy corpus you can ship to a growing cluster without it becoming a maintenance liability.

What Zero Trust Actually Requires at the Network Layer in Kubernetes

Zero trust in the network context has a concrete technical meaning that gets obscured by marketing: no implicit trust based on network location. In a traditional perimeter model, traffic inside the cluster VLAN is implicitly trusted. In a zero-trust model, every connection is authenticated, authorized against an explicit policy, and logged. The key word is every — not just north-south traffic crossing the load balancer at the cluster edge, but east-west pod-to-pod traffic, and ideally node-to-node traffic in transit — all still bounded by the cluster's own network; carrying that trust model across clusters and sites is the separate job of a self-hosted overlay control plane, not the CNI.

In Kubernetes, translating that principle into enforcement requires three things: (1) a cryptographically verifiable identity for every workload that does not rely on IP addresses — which are ephemeral and trivially spoofable within a cluster; (2) policy evaluated and enforced at a kernel level, before traffic reaches the application, so that policy bypass requires compromising the node itself rather than just the pod; and (3) observability that provides a complete audit trail of what communicated with what, when, and with what verdict. The fourth requirement — mTLS for data in transit — is often assumed to be unique to Istio, but it is not. These requirements map directly to Cilium's capability set. The gap — and it is a real one — is in Layer 7 traffic management that goes beyond policy enforcement into request routing, retries, and fault injection.

The eBPF Kernel Architecture: Why In-Kernel Policy Enforcement Changes the Performance Equation

eBPF (extended Berkeley Packet Filter) is a kernel subsystem that allows verified programs to run inside the Linux kernel in response to events, without modifying kernel source code or loading kernel modules. The verification step — run by the kernel's eBPF verifier at load time — ensures programs cannot crash the kernel, leak memory, or loop indefinitely. Cilium compiles its network policy engine to eBPF bytecode and attaches it to specific kernel hook points where it intercepts packets before they ever reach userspace. The performance implication is significant: in the traditional iptables path, every packet triggers a linear traversal of rule chains. eBPF programs are JIT-compiled to native machine code and execute in nanoseconds against pre-built hash map lookups.

The three hook points Cilium uses most extensively are XDP (eXpress Data Path), TC (Traffic Control), and socket/sockops hooks. XDP attaches at the earliest possible point in the receive path — immediately after the NIC driver delivers a packet, before kernel memory allocation for the sk_buff network buffer structure. This is where Cilium performs DDoS mitigation and load-balancing for kube-proxy replacement. TC hooks attach at the Traffic Control ingress and egress queuing discipline layer, just before packets enter the network stack or leave it; this is where Cilium applies L3/L4 identity policy and enforces default-deny. Socket and sockops hooks intercept at the socket layer for L7 traffic redirection — when a connection requires HTTP or gRPC inspection, Cilium redirects it to a per-node Envoy instance (not a per-pod sidecar) via the sockops hook, gets the L7 verdict, and then applies the TC-layer allow or drop.

Cilium intercepts at XDP (earliest ingress), TC (ingress/egress), and sockops (L7 redirect). A single per-node Envoy handles L7 inspection — no per-pod sidecar. Contrast with the traditional model where each pod carries its own proxy.

The practical performance consequence: eBPF programs bypass the linear, per-rule chain traversal that iptables performs as rule count grows, executing instead as JIT-compiled native code against hash-map lookups — a structural advantage that holds regardless of whose datapath is running it. Cilium's own published benchmark against Calico's iptables and eBPF datapaths found the gap widens sharply under connection churn: in a connection-rate test driving 250,000 new TCP connections per second, the system spent between 33% and 90% of available CPU depending on which datapath was under test, with the iptables-based configurations at the high end. Treat that as Cilium's own self-reported number, not an independent measurement, and the benchmark predates several kernel and Cilium releases since — verify against current versions and your own connection profile before sizing a node pool on it. The difference matters less for low-traffic dev clusters and considerably more for API gateway nodes or data-plane services handling tens of thousands of connections per second. The secondary consequence — equally important for security — is that `kube-proxy` replacement means there are no iptables rules to enumerate, inspect, or accidentally weaken through a kubectl flag that bypasses the chain.

Cilium's Network Policy Model: Identity-Based, Not IP-Based

Cilium implements the standard Kubernetes NetworkPolicy API for compatibility, but its native policy object — CiliumNetworkPolicy — is substantially more expressive. The critical architectural difference is that CiliumNetworkPolicy selects endpoints by identity, computed from the pod's label set plus its namespace and service account. When a new pod starts, the Cilium agent on its node assigns it an identity, distributes that identity mapping to BPF maps on all nodes, and the policy evaluation on every node in the cluster is updated automatically. This removes the stale-IP-rule problem endemic to IP-based NetworkPolicy implementations under autoscaling conditions. The same identity model covers pods regardless of runtime — a Wasm workload scheduled under a second RuntimeClass still gets a normal cluster IP and label set, so CiliumNetworkPolicy applies unchanged at the Pod boundary, even though wasi:sockets exposes a narrower socket surface inside it than a Linux container sees.

A concrete L3/L4 policy that enforces namespace-scoped communication with explicit port allowances looks like this:

yaml
# L3/L4 CiliumNetworkPolicy — production-grade default-deny for the api-server service.
# Allows ingress only from pods labeled app=frontend in the same namespace,
# and egress only to the postgres service on port 5432 and to DNS on port 53.
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: api-server-policy
  namespace: production
spec:
  endpointSelector:
    matchLabels:
      app: api-server
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: frontend
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
  egress:
    - toEndpoints:
        - matchLabels:
            app: postgres
      toPorts:
        - ports:
            - port: "5432"
              protocol: TCP
    - toEndpoints:
        - matchLabels:
            "k8s:io.kubernetes.pod.namespace": kube-system
      toPorts:
        - ports:
            - port: "53"
              protocol: UDP
CiliumNetworkPolicy applying default-deny with explicit allowlist. The matchLabels selectors resolve to Cilium identities at enforcement time — IP churn from autoscaling does not require policy updates.

The identity model connects directly to the open-source principle: Cilium's identity computation is transparent, reproducible, and auditable. The identity assigned to any given pod is deterministically derived from its metadata — you can compute it from first principles without running special tooling. This matters for audit evidence: a compliance reviewer can inspect a CiliumNetworkPolicy and trace exactly which workloads are in scope without needing to query runtime BPF maps.

Pod identity flows from labels through a hash to a numeric Cilium identity. CiliumNetworkPolicy references identities, not IP addresses. The BPF policy map enforces the compiled ruleset and Hubble records the verdict for every flow.

L7 Policy Without Sidecars: How Cilium Enforces HTTP and gRPC Rules in the Kernel

The canonical objection to Cilium handling L7 policy has been: "Cilium is a CNI, not a service mesh — it can't parse HTTP headers." This was accurate in 2020. It is not accurate today. Cilium's L7 policy works by redirecting qualifying connections — those that match a toPorts rule with an http or kafka or grpc descriptor — to a per-node Envoy proxy via the sockops hook. The Envoy instance is managed by the Cilium operator, runs on the host network namespace (not in any pod), and handles L7 inspection for all pods on that node. The result is that a single shared Envoy instance on a node with forty pods replaces forty per-pod Envoy sidecars — with the attendant reduction in resource overhead, startup latency, and certificate churn.

An L7 HTTP rule in CiliumNetworkPolicy looks like this:

yaml
# L7 HTTP CiliumNetworkPolicy — restricts the api-server to specific HTTP paths and methods.
# Only GET /api/v1/* from frontend pods is permitted; all other paths are denied at L7.
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: api-server-l7-policy
  namespace: production
spec:
  endpointSelector:
    matchLabels:
      app: api-server
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: frontend
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              - method: "GET"
                path: "^/api/v1/.*"
              - method: "POST"
                path: "^/api/v1/orders$"
    - fromEndpoints:
        - matchLabels:
            app: monitoring
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              - method: "GET"
                path: "^/healthz$"
              - method: "GET"
                path: "^/metrics$"
L7 HTTP CiliumNetworkPolicy. The http.method + http.path rules are evaluated by the per-node Envoy; all other HTTP requests to port 8080 are dropped. No sidecar injection, no certificate authority, no additional control plane component.

The practical limit worth stating clearly: Cilium's L7 policy in CiliumNetworkPolicy enforces allow/deny at the HTTP path and method level. It does not perform advanced traffic management like weighted routing between service versions, circuit breaking on consecutive 5xx responses, or retry budget enforcement. Weighted canary splits sit outside that policy object — use Gateway API HTTPRoute backendRef weights — while circuit breaking and retries still need Istio ambient or a dedicated API gateway. The policy model enforces your security perimeter; traffic management optimizes allowed traffic. Conflating the two is a common source of architectural confusion.

Hubble: What You Get for Observability When Your CNI Is eBPF-Native

Hubble is Cilium's observability layer, and it is the strongest argument for choosing Cilium even in environments that do not immediately need L7 policy. Because Cilium owns the packet path at the kernel level, Hubble can emit a verdict-annotated flow record for every TCP connection and UDP flow in the cluster — source identity, destination identity, destination port, L7 method/path if applicable, verdict (forwarded/dropped), the drop category, and — when policy correlation is on (the default) — the CiliumNetworkPolicy names that produced the verdict. No sampling. No instrumentation required in the application. No sidecar required.

The practical implications for zero-trust operations: (1) You get a complete audit trail of lateral movement attempts. When a pod that should only talk to the database attempts a connection to the Kubernetes API server, Hubble records it with the DROP verdict, the drop category (e.g. POLICY_DENIED), and the policy names in ingress_denied_by / egress_denied_by that blocked it, with nanosecond timestamps. (2) Policy authoring is dramatically safer. Before enforcing a new CiliumNetworkPolicy in deny mode, you can run it in audit mode and observe Hubble flows that would be affected — confirming the policy matches your intent before it breaks production traffic. (3) The Hubble CLI provides real-time flow inspection:

bash
# Stream flows for a specific pod, showing L7 HTTP verdicts
hubble observe --pod production/api-server-6b7d9f-x4p2q --follow

# Show all DROPPED flows cluster-wide in the last 5 minutes
hubble observe --verdict DROPPED --since 5m

# Filter to a specific namespace and show L7 details
# --output jsonpb encodes each event as GetFlowsResponse JSON (has a .flow object).
# --output json aliases jsonpb by default; bare Flow JSON only if -o json is used
# with HUBBLE_COMPAT=legacy-json-output (pre-v0.11 CLI behavior).
hubble observe --namespace production --follow \
  --output jsonpb | jq '.flow | {src: .source.labels, dst: .destination.labels, verdict: .verdict, l7: .l7}'

# Identify which policy is responsible for drops (policy audit)
# dst_port lives under .l4 (Endpoint has no port field); denied_by names the CNP.
hubble observe --verdict DROPPED --namespace production \
  --output jsonpb | jq '.flow | {src_identity: .source.identity, dst_port: (.l4.TCP.destination_port // .l4.UDP.destination_port), reason: .drop_reason_desc, denied_by: (.ingress_denied_by // .egress_denied_by)}'
Hubble CLI flow inspection. --output jsonpb emits GetFlowsResponse JSON with a flow object for jq (--output json is the same alias unless HUBBLE_COMPAT=legacy-json-output). drop_reason_desc is the drop category (e.g. POLICY_DENIED); ingress_denied_by / egress_denied_by name the CiliumNetworkPolicy that produced the verdict (policy correlation is on by default).

Hubble's Relay component aggregates flows from all nodes and exposes them via a gRPC API that the hubble observe CLI queries. Hubble Enterprise (the Isovalent commercial offering) adds multi-cluster aggregation and a time-series storage backend — relevant for regulated environments where flow logs must be retained for 90+ days. The open-source Hubble stores flows in a per-node ring buffer (configurable size, typically 4096 flows); long-term retention in the open-source stack means tailing that gRPC API into your existing log store.

The official `cilium/hubble-otel` adapter has been archived since 20 June 2024 and there is no Hubble receiver in the OpenTelemetry Collector, so durable retention means consuming the Hubble Relay gRPC API directly (or hubble observe -o jsonpb) and shipping flows with your own exporter — not a turnkey Hubble→OTel bridge. That still serves capabilities that need network-flow evidence next to application traces.

WireGuard Encryption: Node-to-Node Transparent Encryption Without Certificate Management

The most operationally underrated feature in Cilium's security stack is transparent WireGuard encryption. Enabled by a single Helm value, it establishes a WireGuard tunnel between every pair of nodes in the cluster, encrypting all pod-to-pod traffic in transit at the node level. The Cilium operator automatically manages WireGuard public key exchange — nodes advertise their public keys via the Kubernetes API server, and Cilium configures the WireGuard interfaces accordingly. There is no PKI, no certificate authority, no certificate rotation schedule, no cert-manager dependency, and no CRL infrastructure to operate.

yaml
# Cilium Helm values to enable WireGuard transparent encryption
# All node-to-node pod traffic is encrypted; no application changes required.
encryption:
  enabled: true
  type: wireguard
  nodeEncryption: true          # Also encrypt node-to-node, pod-to-node and node-to-pod
                                # traffic (beta; nodes labelled node-role.kubernetes.io/control-plane
                                # opt out automatically)
  wireguard:
    userspaceFallback: false    # Require kernel WireGuard (Linux >= 5.6)
    persistentKeepalive: 0      # 0 = disabled; set to 25 if NAT traversal needed
Cilium Helm values for WireGuard node-to-node encryption. Key exchange is automatic via the Kubernetes API. No cert-manager, no CA, no rotation schedule. Requires Linux kernel 5.6+ on all nodes (Ubuntu 22.04 LTS ships 5.15).

WireGuard in this model is node-to-node, not pod-to-pod at the TLS layer. Every byte leaving a node's WireGuard interface bound for another node is encrypted. The security property you gain is protection against passive eavesdropping on the cluster's physical or virtual network — the threat model covering compromised hypervisor NICs, shared-tenant cloud networks, or a threat actor with span-port access to the switch fabric. What WireGuard does not provide: mutual authentication at the application/pod identity level (a pod on node A impersonating a different pod's identity is not prevented by WireGuard alone — that is the CiliumNetworkPolicy identity enforcement layer's job). The two controls are complementary: WireGuard encrypts the channel; Cilium identity policy enforces authorization within the channel.

The question is not whether Cilium or Istio is better. The question is whether the security guarantee you need is 'encrypted and policy-enforced channel between known workload identities' or 'traffic management, retries, and external authz on top of that.' The former is Cilium. The latter is Cilium plus something. Knowing which problem you are solving before you architect the solution is the job.
Stribog engineering practice

Cilium vs Calico: The Comparison Is Not eBPF Versus iptables

Calico is the incumbent CNI in a large share of Kubernetes clusters, and the usual framing of this choice — eBPF against iptables — has been wrong since Calico shipped an eBPF dataplane as a technology preview in v3.13 (2020). Calico can run several dataplanes: the standard Linux one (iptables), eBPF, VPP, and more recently nftables — check that last one’s maturity against the release you actually deploy. Choosing on "does it use eBPF" answers a question neither project has disagreed about for five years.

The divergence is the identity model, and it is architectural rather than a feature gap. Calico's policy engine is an extension of the Kubernetes model: selectors resolve to endpoints, and rules are programmed per-node against those endpoints. Cilium allocates a numeric security identity per unique label set, distributes it cluster-wide, and carries it in the datapath — which is what section 3 described. The practical consequence is where policy cost lands as the cluster grows: Calico's per-node rule programming scales with the endpoints a node needs to reason about, Cilium's with the number of distinct *identities*. In a cluster with many replicas of few workload types, identities stay flat while endpoints do not.

Two things commonly claimed as Cilium exclusives are not. Calico has shipped WireGuard node-to-node encryption since [v3.14](https://docs.tigera.io/archive/v3.14/release-notes/) (introduced as a technology preview; check the current maturity of the feature against the release you deploy), so the argument in section 6 is an argument against certificate-managed mesh encryption generally, not against Calico. And Calico's BGP implementation is the older and more battle-tested of the two — it long predates Cilium's BGP Control Plane, and on a bare-metal fabric where you are peering with top-of-rack switches that maturity is a real consideration, not a tie-breaker.

  • L7 policy. Cilium enforces HTTP/gRPC/Kafka rules in open source via its embedded Envoy. Calico's L7 capabilities in the open-source project are narrower, and its richer application-layer policy and flow visualisation have historically been part of the commercial editions. Verify against the version you are deploying — this boundary has moved.
  • Flow observability. Hubble ships with Cilium open source and is the reason the audit argument in section 5 works. Calico Open Source has added flow-visualisation tooling, but Hubble's identity-labelled flow record is the more direct fit for an evidence trail.
  • Kubernetes-native surface. Both implement standard NetworkPolicy. Both add a CRD with cluster-scope and richer matching — CiliumClusterwideNetworkPolicy, GlobalNetworkPolicy. Policy written against the standard object is portable between them; policy written against either CRD is not.
  • Managed-platform reality. GKE Dataplane V2 is Cilium. AKS offers both Azure CNI Powered by Cilium and Calico. EKS defaults to the AWS VPC CNI, which has enforced Kubernetes NetworkPolicy natively since 2023; Calico is layered on where richer policy is needed. On managed control planes the choice is frequently made for you, and — see the FAQ — a vendor-integrated Cilium is not the full upstream feature set.

Where Istio Ambient Mode Still Wins: Traffic Management Beyond Policy

Istio's ambient mesh mode reached General Availability in Istio 1.24 (7 November 2024), replacing per-pod sidecars with a node-level ztunnel proxy for L4 mTLS and a per-namespace waypoint proxy for L7 traffic management. This dramatically reduced Istio's resource overhead — the primary operational objection to Istio in 2020–2024 — while preserving what Cilium's policy model does not: DestinationRule circuit breaking and connection pool management, and ext_authz integration with external authorization services like OPA/Cedar. (Weighted canary splits Cilium already covers via Gateway API HTTPRoute backendRef weights; Istio's VirtualService is an alternative path, not the only one.)

Istio ambient mode composes cleanly with Cilium as the CNI: Cilium handles L3/L4 identity policy, kube-proxy replacement, and WireGuard node encryption; Istio ambient's ztunnel handles pod-level mTLS with SPIFFE/X.509 identity; waypoint proxies handle L7 traffic management where needed. This combination gives you the full stack, at the cost of operating both Istiod and Cilium's operator. Whether that complexity is justified depends on whether DestinationRule circuit breaking and ext_authz are real production requirements — not theoretical ones — versus what Gateway API already covers for weighted canaries.

Capability matrix. Cilium standalone covers the security requirements for most workloads. Cilium + Istio ambient adds traffic management at medium complexity. Istio sidecar adds per-pod resource overhead at high control-plane complexity. Self-select based on actual requirements, not perceived compliance need.

The honest recommendation for a new cluster: start with Cilium standalone. Run the default-deny policy corpus described in the next section. Add Hubble and wire its gRPC API into your log pipeline. Enable WireGuard. Revisit in 90 days and ask: which of circuit breaking, ext_authz, or SPIFFE identity are operational requirements — not theoretical ones? In most cases the answer is zero or one — and if one, evaluate whether a dedicated API gateway at the ingress layer handles it with less cluster-wide operational surface than Istiod.

Designing a Zero-Trust Network Policy Corpus That Survives Cluster Growth

A default-deny Cilium policy corpus that remains maintainable as the cluster grows needs three structural properties: (1) namespace-level default deny as the baseline, applied via a cluster-scoped CiliumClusterwideNetworkPolicy; (2) service-specific allowlists as CiliumNetworkPolicy objects that live alongside the service's Helm chart or Kustomize overlay, owned by the team that owns the service; and (3) shared infrastructure allow rules — for DNS, the kube-apiserver, metrics scraping, and log collection — managed centrally and version-controlled. The failure mode of a flat policy corpus (all policies in one place, owned by the platform team) is that it becomes a bottleneck: every new service needs a platform team PR to add its allowlist. The failure mode of a fully delegated corpus (teams write their own policies without review) is policy drift and gaps.

yaml
# Cluster-wide default-deny baseline + shared infrastructure allows.
# A policy with neither an ingress: nor an egress: section has no effect at all.
# Default-deny engages per direction when a selecting policy carries that section.
# IMPORTANT: ingress: [{}] or egress: [{}] is a wildcard ALLOW, not a deny.
# Exclude kube-system so CoreDNS and other control-plane pods are not locked out.
#
# Step 1: Egress default-deny + DNS allow for non-system pods.
# Aligns with Cilium's shipped example (v1.19.6 tag):
# examples/kubernetes/servicemesh/policy/default-deny.yaml —
# NotIn kube-system, egress-only DNS allow, L7 dns matchPattern.
apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: default-deny-egress-allow-dns
spec:
  description: "Block all egress except DNS by default (non-system pods)"
  endpointSelector:
    matchExpressions:
      - key: io.kubernetes.pod.namespace
        operator: NotIn
        values:
          - kube-system
  egress:
    - toEndpoints:
        - matchLabels:
            io.kubernetes.pod.namespace: kube-system
            k8s-app: kube-dns
      toPorts:
        - ports:
            - port: "53"
              protocol: UDP
          rules:
            dns:
              - matchPattern: "*"
---
# Step 2: Shared infrastructure — allow Prometheus scraping on app pods and
# engage ingress default-deny for non-system namespaces only.
# Do NOT use endpointSelector: {} here: that would put kube-dns under ingress
# default-deny with no port-53 allow and break cluster DNS.
# Adjust ports to match your application metrics endpoints
# (common: 9091, 8080, 2112). Add only ports your pods actually expose.
apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: allow-metrics-ingress
spec:
  endpointSelector:
    matchExpressions:
      - key: io.kubernetes.pod.namespace
        operator: NotIn
        values:
          - kube-system
  ingress:
    - fromEndpoints:
        - matchLabels:
            app.kubernetes.io/name: prometheus
      toPorts:
        - ports:
            - port: "9091"
              protocol: TCP
            - port: "8080"
              protocol: TCP
Default-deny policy corpus. Step 1 follows Cilium's shipped egress default-deny + DNS allow (NotIn kube-system). Step 2 scopes ingress default-deny the same way so kube-dns stays reachable. A policy with neither ingress: nor egress: has no effect; ingress: [{}] or egress: [{}] is a wildcard ALLOW. Service-specific allowlists (like the api-server example above) are namespace-scoped.

The structural recommendation that survives cluster growth: make the policy corpus part of the service contract, not the platform contract. The platform team owns the cluster-wide default-deny baseline and the shared infrastructure rules. Every service owns its own CiliumNetworkPolicy in its own namespace, checked into its own repository, reviewed by the service team. The platform team provides a policy template and a kyverno test-compatible test harness (see the capabilities page for our policy governance pattern). New services scaffold their policy from the template as part of onboarding — they cannot deploy without one, because the cluster-wide default-deny blocks any undeclared traffic. The same identity-based egress policy applies cleanly to CI runner pods: self-hosted runners on Kubernetes with ARC become ordinary cluster workloads, and Cilium can restrict their egress to only the registry and the API server they need.

The long-game property of this architecture: because Cilium identities are derived from labels rather than IPs, and because the policy objects are YAML checked into version control, the corpus remains legible and auditable across team changes, cluster rebuilds, and Kubernetes version upgrades. The BPF maps are runtime state; the CiliumNetworkPolicy objects are the source of truth. A new engineer joining the team can read the policy for any service and understand its communication surface completely — no need to query runtime state, decode iptables rules, or reverse-engineer firewall logs.

The thesis of engineering sovereignty runs through this architecture at every layer. The eBPF programs are open-source and auditable. The policy objects are YAML in your repository, not opaque SaaS configuration. The WireGuard keys are managed by code you control. The Hubble flow records go to your observability stack, not a vendor's cloud. When a security auditor asks "how do you enforce east-west network segmentation?" the answer is a set of CRD objects in a Git repository with a commit history — not a screenshot from a vendor's console and a hope that the SaaS configuration hasn't drifted.

Putting It Together: The Minimum Viable Zero-Trust Cilium Stack

For a cluster starting from scratch or migrating from a flat-network CNI, the recommended deployment order is: (1) Install Cilium with kube-proxy replacement and WireGuard encryption enabled. (2) Deploy Hubble Relay and wire its gRPC API into your log pipeline. (3) Apply the default-deny corpus above in audit mode. (4) Run Hubble in audit mode for one full week — longer if the cluster has batch workloads with irregular traffic patterns — and build the service-specific allowlists from observed flows. (5) Switch the cluster-wide policy to enforcement mode namespace by namespace, starting with non-critical workloads. (6) Enable L7 HTTP rules for services where path-level policy is a security requirement, not a curiosity.

The result is a cluster where every east-west connection is authorized by an explicit policy rule derived from workload identity, encrypted in transit by WireGuard, observable at L3/L4/L7 by Hubble, and auditable from a Git-controlled YAML corpus. That satisfies the technical requirements of zero trust for the overwhelming majority of Kubernetes workloads — without a certificate authority, without per-pod sidecar overhead, and without an Istio control plane to operate. Whether you add Istio ambient on top for traffic management is an operational decision about specific capabilities, not a compliance decision about whether your network is zero-trust.

The long game here is a cluster that does not require deep Cilium expertise to operate day-to-day. The policy corpus is readable YAML. Flow logs land in your pipeline via Hubble's gRPC API. The encryption is a Helm value. When the engineer who built the original Cilium installation moves to a different team, the cluster's security posture survives because the specification is in version control, not in their head.

§FAQ/Common questions

Frequently asked

Cilium vs Calico — which should I choose for a new bare-metal cluster?

Choose on the identity model and the evidence requirement, not on eBPF: Calico has had an eBPF dataplane since v3.13 and WireGuard node encryption since v3.14, so those are not differentiators. Cilium wins when you need identity-based policy carried in the datapath, L7 (HTTP/gRPC/Kafka) enforcement without sidecars, and Hubble's identity-labelled flow record as an audit trail — the three things this article is about. Calico wins when your policy corpus is standard NetworkPolicy, your team already runs it, or you are peering BGP with top-of-rack switches, where Calico's implementation is the older and more battle-tested of the two. On a managed control plane the decision is often already made: GKE Dataplane V2 is Cilium, AKS offers both, EKS defaults to the AWS VPC CNI, which enforces NetworkPolicy natively, with Calico layered on for richer policy.

Does Cilium replace Istio entirely for a zero-trust Kubernetes deployment?

For the security requirements — workload identity, policy enforcement, encryption in transit, and observability — yes, Cilium covers them without Istio. What Cilium does not provide is Istio's traffic management features: circuit breaking, retries, fault injection, and ext_authz integration with external authorization services. (Weighted traffic splitting for canaries Cilium does have, via Gateway API HTTPRoute backendRef weights.) If you need the remaining capabilities, Cilium and Istio ambient mode compose cleanly — Cilium handles CNI and L3/L4 policy; Istio ambient handles pod-level mTLS and L7 traffic management via waypoint proxies.

What Linux kernel version does Cilium with WireGuard require?

WireGuard is built into the Linux kernel from version 5.6 onwards. Ubuntu 22.04 LTS ships kernel 5.15; Ubuntu 24.04 LTS ships kernel 6.8. Both are fully supported. If your nodes run kernels older than 5.6 (notably Ubuntu 20.04 with a stock kernel below 5.6), WireGuard requires the backported module or a kernel upgrade before enabling Cilium's transparent encryption.

How does Cilium's L7 policy differ from Istio's authorization policy at L7?

Both enforce HTTP method and path allow/deny rules. The primary operational difference is that Cilium's L7 enforcement uses a per-node shared Envoy instance (zero per-pod overhead); Istio sidecar mode injects a full Envoy proxy into every pod (roughly 50–250 MB per pod). Istio ambient mode's waypoint proxy reduces this to a per-namespace proxy, similar to Cilium's per-node model in resource footprint. Feature differences: Istio also supports request header mutation, JWT validation inline, and ext_authz delegation — Cilium's L7 policy does not.

Can CiliumNetworkPolicy co-exist with standard Kubernetes NetworkPolicy objects?

Yes. Cilium honors both Kubernetes NetworkPolicy and CiliumNetworkPolicy objects simultaneously. The effective policy is the union of all applicable rules (most permissive wins per allowed direction). For a clean default-deny posture, apply the CiliumClusterwideNetworkPolicy baseline and use CiliumNetworkPolicy for all service-specific rules rather than mixing both API types, which simplifies auditing.

GKE Dataplane V2 uses Cilium — does that mean I get the full Cilium feature set on GKE?

GKE Dataplane V2 uses Cilium's eBPF dataplane for L3/L4 network policy and kube-proxy replacement, but it exposes a managed subset of Cilium's capabilities through GKE's API rather than the full CiliumNetworkPolicy CRD. Hubble observability is available but requires enabling the GKE Network Policy Logging feature. L7 HTTP policy and direct CiliumNetworkPolicy authoring require running Cilium in self-managed mode via the Helm chart — this is supported on GKE Standard clusters, not GKE Autopilot (Autopilot does not permit custom CNI plugins). AKS and EKS managed Cilium offerings have similar scope-of-feature considerations.

How do Hubble flow logs relate to a NIS2 or SOC 2 audit requirement for network logging?

Hubble provides connection-level flow logs with workload identity, verdict, and timestamp for every TCP connection and UDP flow in the cluster — this satisfies the 'network traffic logging' evidence category common in NIS2 and SOC 2 Type II audits. For retention, the default in-memory ring buffer is not sufficient for audit purposes; pipe Hubble flows via the gRPC API to a durable store (Loki, Elasticsearch, or S3-backed OTel pipeline). Preserve at least 90 days of flow data and ensure the storage backend is inside your audit boundary, not a shared SaaS observability platform.

cilium vs calicoCilium eBPF Kubernetes zero trust networking 2026Cilium vs Istio service mesh 2026eBPF Kubernetes network policyzero trust Kubernetes networkingservice mesh without sidecars

Executive Briefing

Thirty minutes to clarify your infrastructure risk

Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.