sz

Sejan

Blog

Sidecarless Service Mesh with eBPF: Cilium, Ambient Mesh, and Socket-Level Kubernetes Networking

Home › How to › Sidecarless Service Mesh with eBPF: Cilium, Ambient Mesh, and Socket-Level Kubernetes Networking
Sidecarless Service Mesh with eBPF: Cilium, Ambient Mesh, and Socket-Level Kubernetes Networking

For years, the gold standard for Kubernetes microservice security, observability, and traffic routing was the Sidecar Service Mesh. By injecting an Envoy proxy container into every single application Pod, platforms like traditional Istio and Linkerd delivered mutual TLS (mTLS), distributed tracing, and canary routing without modifying application code.

However, as Kubernetes clusters scaled to thousands of microservices, the operational reality of the sidecar pattern caught up with platform engineering teams: the dreaded "Sidecar Tax". Running thousands of dedicated proxy containers consumes tens of gigabytes of stranded RAM, adds significant CPU overhead, complicates pod lifecycle ordering, and introduces 2 to 6 milliseconds of latency per microservice hop due to repetitive TCP/IP stack traversals and iptables redirection.

The industry's answer is Sidecarless Service Mesh, driven by the revolutionary power of Extended Berkeley Packet Filter (eBPF) in the Linux kernel and architectural splits like Istio Ambient Mesh and Cilium Service Mesh.

By moving Layer 4 encryption and routing directly into the Linux kernel socket layer (eBPF sockops) and sharing lightweight, out-of-pod proxies only when Layer 7 inspection is strictly required, sidecarless architectures slash resource consumption by up to 80% and restore near-native network latencies.

In this guide, we dissect the mechanics of socket-level kernel redirection, explore the architecture of Cilium and Istio Ambient Mesh, examine kernel eBPF C programs, and evaluate production benchmark comparisons.


The Anatomy of the "Sidecar Tax"

To understand why the industry is moving away from sidecars, consider the journey of a single HTTP request in a traditional sidecar service mesh:

flowchart LR
    subgraph Client Pod
        AppA[Client App] -->|1. Local Socket| IPT1[iptables NAT]
        IPT1 -->|2. Loopback TCP| EnvoyA[Client Sidecar Envoy]
    end

    EnvoyA -->|3. Node Virtual Ethernet| HostNet[Host Linux Kernel Network & eth0]
    HostNet -->|4. Physical Network / Overlay| DestHost[Destination Host Kernel]

    subgraph Destination Pod
        DestHost -->|5. veth Pair| IPT2[iptables NAT]
        IPT2 -->|6. Inbound TCP| EnvoyB[Server Sidecar Envoy]
        EnvoyB -->|7. Local Socket| AppB[Server App]
    end

    style EnvoyA fill:#e11d48,stroke:#fda4af,color:#fff
    style EnvoyB fill:#e11d48,stroke:#fda4af,color:#fff
    style IPT1 fill:#f97316,stroke:#fdba74,color:#fff
    style IPT2 fill:#f97316,stroke:#fdba74,color:#fff
    style AppA fill:#0284c7,stroke:#38bdf8,color:#fff
    style AppB fill:#059669,stroke:#6ee7b7,color:#fff

The Four Penalties of Sidecars

  1. The Latency Penalty (4x TCP Stack Traversal): A single request traverses the Linux network stack and iptables chains four separate times between two pods on the same cluster. Each hop involves memory copies between kernel space and user space, TCP handshake overhead, and context switching.
  2. The Memory Tax (Stranded RSS): Each Envoy sidecar requires 40MB to 120MB of baseline memory. In a cluster with 500 pods, sidecars alone consume 20GB to 60GB of RAM purely for proxy scaffolding before handling any application traffic.
  3. Application Lifecycle Entanglement: Because the sidecar runs as a co-located container in the same pod, race conditions abound: applications start before Envoy is initialized (causing network drops during boot), and batch jobs hang because the sidecar refuses to terminate after the main container exits.
  4. Invasive Upgrades: Updating the service mesh data plane version requires restarting every single application pod in the cluster, forcing disruptive rolling restarts.

The eBPF Socket Layer Revolution: Bypassing the TCP/IP Stack

eBPF allows sandboxed C code to execute safely inside the Linux kernel at native speed upon event triggers. When applied to Kubernetes networking, eBPF revolutionizes how pods communicate on the same node through Socket Layer Redirection (sockops).

Instead of routing packets down through virtual ethernet devices (veth), bridge interfaces, iptables tables, and back up through the TCP/IP stack, eBPF attaches programs to socket operations (BPF_PROG_TYPE_SOCK_OPS and BPF_PROG_TYPE_SK_MSG):

flowchart TD
    subgraph Traditional Kubernetes Network Path
        Pod1_Old[Pod A User Space] --> Sock1_Old[Pod A Socket]
        Sock1_Old --> TCP1[TCP/IP Stack]
        TCP1 --> IPT_Old[iptables / netfilter]
        IPT_Old --> Veth1[veth0]
        Veth1 --> Bridge[cbr0 / Bridge]
        Bridge --> Veth2[veth1]
        Veth2 --> TCP2[TCP/IP Stack]
        TCP2 --> Sock2_Old[Pod B Socket]
        Sock2_Old --> Pod2_Old[Pod B User Space]
    end

    subgraph eBPF Sockops Direct Path (Zero TCP Stack)
        Pod1[Pod A User Space] --> Sock1[Pod A Socket]
        Sock1 ==>|eBPF bpf_msg_redirect_hash (Direct Memory Copy)| Sock2[Pod B Socket]
        Sock2 --> Pod2[Pod B User Space]
    end

    style Sock1 fill:#0284c7,stroke:#38bdf8,color:#fff
    style Sock2 fill:#059669,stroke:#6ee7b7,color:#fff
    style Pod1_Old fill:#475569,stroke:#94a3b8,color:#fff
    style Pod2_Old fill:#475569,stroke:#94a3b8,color:#fff

When Pod A establishes a TCP connection to Pod B on the same node, the eBPF sockops program captures the 4-tuple (source IP/port, destination IP/port) and stores the socket file descriptor in a kernel BPF_MAP_TYPE_SOCKHASH map. When Pod A writes data, bpf_msg_redirect_hash() directly copies the socket buffer into Pod B's socket receive queue—completely bypassing the entire TCP/IP network stack and iptables!


C Architecture: Under the Hood of eBPF Socket Redirection

Here is how the kernel-level eBPF C code achieves zero-overhead socket redirection:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

// BPF Map storing socket references indexed by 4-tuple key
struct {
    __uint(type, BPF_MAP_TYPE_SOCKHASH);
    __uint(max_entries, 65535);
    __type(key, struct sock_key);
    __type(value, __u64);
} sock_map SEC(".maps");

struct sock_key {
    __u32 sip;
    __u32 dip;
    __u32 sport;
    __u32 dport;
};

// 1. SOCK_OPS program: Triggered when TCP connection is established
SEC("sockops")
int bpf_sockmap(struct bpf_sock_ops *skops) {
    if (skops->family != AF_INET)
        return 0;

    // Intercept active or passive connection establishment
    if (skops->op == BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB ||
        skops->op == BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB) {
        
        struct sock_key key = {
            .sip = skops->local_ip4,
            .dip = skops->remote_ip4,
            .sport = bpf_ntohl(skops->local_port),
            .dport = skops->remote_port,
        };

        // Insert socket into kernel hash map
        bpf_sock_hash_update(skops, &sock_map, &key, BPF_NOEXIST);
    }
    return 0;
}

// 2. SK_MSG program: Triggered on socket sendmsg() to redirect payload
SEC("sk_msg")
int bpf_redir(struct sk_msg_md *msg) {
    struct sock_key key = {
        .sip = msg->remote_ip4,
        .dip = msg->local_ip4,
        .sport = msg->remote_port,
        .dport = bpf_ntohl(msg->local_port),
    };

    // Directly redirect packet buffer into destination socket queue
    return bpf_msg_redirect_hash(msg, &sock_map, &key, BPF_F_INGRESS);
}

char _license[] SEC("license") = "GPL";

Architectural Approaches: Cilium vs. Istio Ambient Mesh

Two dominant philosophies have emerged for delivering sidecarless service meshes in production:

flowchart TD
    subgraph Cilium Service Mesh (eBPF Native)
        C_L4[eBPF Kernel Layer: L4 Routing, WireGuard mTLS, Security Policy]
        C_L7[Shared Node-Level Envoy Daemon: L7 HTTP Filtering only when declared]
        C_L4 -->|L7 needed| C_L7
    end

    subgraph Istio Ambient Mesh (Layer 4 / Layer 7 Separation)
        A_Ztunnel[Node-Level ztunnel: Rust-based L4 HBONE Encapsulation + mTLS]
        A_Waypoint[Dedicated Waypoint Proxy: Out-of-pod Envoy per Service/Namespace for L7]
        A_Ztunnel -->|L7 policy configured| A_Waypoint
    end

    style C_L4 fill:#0284c7,stroke:#38bdf8,color:#fff
    style C_L7 fill:#059669,stroke:#6ee7b7,color:#fff
    style A_Ztunnel fill:#7c3aed,stroke:#c084fc,color:#fff
    style A_Waypoint fill:#10b981,stroke:#34d399,color:#fff

1. Cilium Service Mesh (eBPF-First)

  • Kernel-Level Enforcement: L3/L4 network policies, load balancing (replacing kube-proxy), and cryptographic encryption (via native WireGuard or IPsec) run 100% inside the Linux kernel via eBPF.
  • On-Demand Envoy: When an application requires L7 HTTP header inspection, JWT verification, or path-based canary routing, Cilium dynamically diverts traffic to a single shared Envoy proxy per worker node. If no L7 rules exist, Envoy is never touched.

2. Istio Ambient Mesh (ztunnel + Waypoint Proxies)

  • Separation of Concerns: Splits service mesh functionality into two distinct layers:
    1. L4 Secure Overlay (ztunnel): A fast, lightweight Rust daemon running on each node that establishes mutual TLS using HBONE (HTTP-Based Overlay Network Environment) over port 15008 and enforces L4 network authorization.
    2. L7 Processing (Waypoint Proxies): Standard Envoy instances running as independent Kubernetes deployments (per namespace or per service account). Waypoints scale dynamically without being bound to application pods.
  • Zero Disruption: Enabling Ambient Mesh on an existing namespace requires adding a single label (istio.io/dataplane-mode=ambient)—no pod restarts or container reinjections required!

Production Comparison: Traditional Sidecars vs. Sidecarless

Metric / Capability Traditional Sidecar (Istio / Linkerd) Istio Ambient Mesh Cilium Service Mesh (eBPF)
Memory Footprint (500 Pods) ~30 GB – 50 GB RAM ~2 GB – 5 GB RAM (ztunnel + waypoints) ~1.5 GB – 3 GB RAM (eBPF + Node Envoy)
P99 Latency Overhead (L4) +2.5 ms – 5.0 ms +0.3 ms – 0.6 ms (ztunnel HBONE) < 0.1 ms (eBPF sockops redirection)
Pod Restart on Mesh Upgrade Yes (Every pod must rolling-restart) No (DaemonSet ztunnel upgrade is transparent) No (Kernel eBPF program atomic replace)
Transport Encryption Envoy mTLS (TLS in user space) ztunnel HBONE mTLS (Rust-native) Kernel WireGuard / IPsec or Node Envoy mTLS
CVE Patching Surface Vulnerable per-pod proxies Centralized ztunnel + waypoint deployment Centralized per-node kernel modules

Summary & Architectural Insights

  • The Sidecar Era is Concluding: Running an isolated proxy per application container wastes untenable amounts of CPU and memory while introducing avoidable network latency hops.
  • eBPF Unlocks Socket Bypass: By rewriting socket queues with sockops in the Linux kernel, node-local microservice traffic avoids TCP/IP stack overhead entirely.
  • L4 and L7 Must Be Separated: 90% of microservice traffic only requires L4 mutual TLS, cryptographic identity, and basic transport authorization. Offloading L4 to eBPF/ztunnel and reserving Envoy only for complex L7 routing provides the optimal balance of efficiency and flexibility.
  • Non-Invasive Operations: Sidecarless architectures allow platform teams to upgrade, patch, and manage service mesh infrastructure without restarting or entangling application workloads.

Comments (0)

Leave a Reply ↓

No comments yet. Be the first to start the discussion!

Leave a Comment