What Is a Sidecar-less Service Mesh? What It Solves, What You Gain, and a Cilium Lab with WireGuard and Mutual Auth

Sidecar-less service mesh explained with a Cilium lab: WireGuard took readable HTTP on the wire from 6 lines to 0, plus SPIFFE mutual auth, Hubble L7 and GAMMA.

By Mirac Can Yılmaz 10 min read
What Is a Sidecar-less Service Mesh? What It Solves, What You Gain, and a Cilium Lab with WireGuard and Mutual Auth - platform-engineering yazısının kapak görseli

In most Kubernetes clusters, traffic between pods on different nodes is not encrypted. I ran a packet capture on a node of my lab cluster and read the HTTP requests between two pods in plain text. The usual answer is a service mesh, and a service mesh usually means one extra proxy container in every pod. In this post I look at what a sidecar-less service mesh is, what it solves and what you gain, and then I show the lab where Cilium did the mesh jobs without a single sidecar: encryption, identity, HTTP visibility and a traffic split. It broke in a few places; those are at the end, with the real messages.

What is a sidecar-less service mesh?

A service mesh is a layer that handles four things for the services in a cluster, so the applications don't have to:

  • Encryption: traffic between services can't be read on the network.
  • Identity: a service only accepts requests from services it knows.
  • Visibility: you can see which service called which, with method, path and status code.
  • Traffic management: splitting traffic between versions, routing by header.

The classic way (Istio, Linkerd) puts a proxy sidecar container next to every application container. All traffic goes through it.

A sidecar-less mesh does the same jobs once per node instead of once per pod. In Cilium, encryption happens in the Linux kernel with WireGuard, HTTP-level work goes through one Envoy per node (cilium-envoy), and identity comes from SPIFFE through SPIRE. The pods stay as they are.

What problem does it solve?

Traffic in the clear. By default, nothing encrypts pod-to-pod traffic between nodes. Anyone who can see the network (a compromised node, a mirrored switch port, a cloud network tap) can read it. In the lab, one capture on one node showed GET / HTTP/1.1, the Host header and the 200 OK.

The cost of sidecars. A sidecar is one more container in every pod: it is injected on every deploy, it needs memory and CPU in every pod, it must be restarted when the mesh is upgraded, and every kubectl exec asks "which container?". For many teams that cost is the reason they never turn the mesh on.

"Who called whom?" Without a mesh, the network only sees IPs and ports. When something returns 500, you can't see which caller, which path, how long it took.

What you gain with Cilium's sidecar-less mesh

Gain What it means in practice In the lab
Encryption with no change to the apps WireGuard between nodes, in the kernel; pods don't know Readable HTTP lines in the capture: 6 before, 0 after
Small switch Two Helm values and an agent restart 31 s on three nodes
Identity checks between services A policy with authentication.mode: required; agents do a SPIFFE handshake The intruder pod timed out; the client got 200 after a ~1 s handshake
HTTP visibility An L7 rule sends traffic through the node's Envoy; Hubble shows method, path, status GET /headers → 200 2ms per request
Standard traffic split GAMMA: a Gateway API HTTPRoute whose parent is a Service 50 requests at 90/10: 46 / 4
No extra component per pod Envoy and SPIRE agent run per node 0 sidecars

Before deciding, know what you don't get:

Need Cilium 1.20
Retries, timeouts, circuit breaking Only through raw Envoy config (CiliumEnvoyConfig), little validation
A certificate inside the pod (application-level mTLS) No. Encryption happens at the node; the pod never sees a certificate
Identity across clusters No. Mutual auth does not work with Cluster Mesh in 1.20
Long-term mutual auth Deprecated in 1.20, removed in 1.21. The upgrade notes point to ztunnel transparent encryption instead

I still show mutual auth, because the policy model (authentication.mode: required) is what you will meet in the replacement too.

How it works

Diagram: on the left a sidecar mesh with a proxy container in every pod; on the right Cilium with plain app pods and one node layer of WireGuard in the kernel, cilium-envoy and the SPIRE agent. Below, four mesh jobs with lab results: encryption 6 to 0 readable lines, identity: intruder timeout, visibility: GET /headers 200, traffic split 46 of 50 to v1

  • Encryption: every Cilium agent creates a WireGuard interface (cilium_wg0) and shares its public key through Kubernetes. Pod traffic to another node goes through that interface, encrypted, on UDP port 51871.
  • Identity: Cilium already gives every group of pods a numeric identity from their labels. With mutual auth on, the operator registers those identities in SPIRE, and before the first packet between two identities passes, the agents on both sides check each other's SPIFFE certificate.
  • Visibility and traffic split: an L7 rule or an HTTPRoute sends the traffic through cilium-envoy on the node, which reports every request to Hubble and applies weights and header matches.

The lab: what I built

Everything runs on kind on a MacBook (Colima, 4 CPU / 8 GB, kernel 6.8 with the WireGuard module): Kubernetes 1.37, Cilium 1.20.2, SPIRE 1.15.3 from the Cilium chart, Gateway API v1.6.1. Two podinfo versions run on one worker node; a client pod and an intruder pod run on the other, so their traffic has to cross nodes. Pods on the same node never go through WireGuard. The whole lab took about 4 minutes in the last run.

Step 1: Read the traffic, then encrypt it

First the capture without encryption. A small hostNetwork pod with tcpdump on the podinfo node watched eth0 while the client sent three requests. Inside 80 packets there were 6 readable HTTP lines: the request line, the Host and User-Agent headers and the response status.

tcpdump on the node without encryption: VXLAN packets on port 8472, and inside them readable GET / HTTP/1.1, Host podinfo:9898, User-Agent curl and HTTP/1.1 200 OK; 6 HTTP tokens in 80 packets

Then two Helm values (encryption.enabled: true, encryption.type: wireguard) and a restart of the Cilium agents. 31 seconds later every agent reported WireGuard with 2 peers, one for each other node.

make encrypt: all three Cilium agents report Encryption Wireguard on cilium_wg0, port 51871, peers 2

The same capture after that: only UDP packets on port 51871, and 0 readable HTTP lines in 80 packets. The apps did not change, and no pod restarted except the Cilium agents.

The same capture with WireGuard on: UDP packets between nodes on port 51871, no readable HTTP, 0 tokens in 80 packets

Step 2: No identity, no way in

SPIRE comes with the Cilium chart. The server was healthy, all three node agents were attested, and the operator had registered podinfo's Cilium identity as a SPIFFE ID.

make spire: server healthy, 3 attested agents, and an entry for podinfo's identity 18935 with SPIFFE ID spiffe://spiffe.cilium/identity/18935 and parent cilium-operator

Then a policy on podinfo: accept the client, and require authentication. The intruder, which the policy does not allow, timed out. The client got 200, but the first request took about one second: that is the handshake between the two agents. Hubble showed the dropped SYN packets with Authentication required, and the forwarded flows carried auth_type: SPIRE.

authentication.mode required: intruder 000 exit 28 timeout; client 200 in 1.04 s and 1.01 s; Hubble shows Authentication required DROPPED for podinfo-v1 and podinfo-v2 and auth_type SPIRE on 2 forwarded flows

Two requests, two one-second waits: podinfo v1 and v2 carry different labels, so they are two identities, and each one pays its own first handshake. Ten replicas of one Deployment would be one identity and one handshake.

Step 3: See HTTP without a sidecar

An L7 rule that allows all HTTP on port 9898 is enough to send that traffic through the node's Envoy. After that Hubble showed every request with method, URL, status and time.

Hubble with the L7 rule: http-request GET http://podinfo:9898/headers and http-response HTTP/1.1 200 with 0 to 2 ms, for podinfo-v1 and podinfo-v2

Step 4: Split traffic inside the cluster with GAMMA

GAMMA is the part of Gateway API for traffic inside the cluster: an HTTPRoute whose parent is a Service instead of a Gateway. With weights 90 and 10, 46 of 50 requests went to v1 and 4 to v2 (44/6 and 45/5 in earlier runs). With a second rule, requests with the header x-canary: true went to v2.

GAMMA: the HTTPRoute is Accepted and ResolvedRefs; 50 requests counted 46 v1 and 4 v2; with the x-canary header the answer is v2, without it v1

Final check

make verify: Cilium, Envoy DaemonSet OK; all three agents on WireGuard; both policies valid; the HTTPRoute and its generated CiliumEnvoyConfig exist; 20 requests all to v1 because the header rule is still in place

Where it broke: 3 real errors

1. SPIRE server stuck at 1/2: Stopping Server APIs right after Starting Server APIs

With the chart defaults, the SPIRE server never became ready and the node agents waited in Init:Error. The log had no real error. The cause: the server runs as uid 1000, but its socket directory is a host path created as root, so binding the socket fails silently (cilium/cilium#40533). The fix in the lab's values is to run the server as root (securityContext.runAsUser: 0), the way the chart behaved before SPIRE 1.9.

First attempt: spire-server-0 stuck at 1/2 with the readiness probe refused, agents in Init:Error; log Starting Server APIs then Stopping Server APIs and Notifier failed; cause uid 1000 vs a root socket path, fix runAsUser 0

2. The setting says on, the agents say Encryption: Disabled

helm upgrade with encryption on returned STATUS: deployed, and the Cilium ConfigMap said enable-wireguard: true. All three agents still said Disabled. Helm only rewrites the configuration; the agents read it when they start. After a restart of the cilium DaemonSet (27 s) all three were on WireGuard. Nothing warns you about this. If you don't check the agents, you think traffic is encrypted when it isn't.

helm upgrade with encryption enabled: STATUS deployed, ConfigMap enable-wireguard true, but all three agents Encryption Disabled; after rollout restart in 27 s all three show Wireguard with 2 peers

3. failed to get certificate for local identity: the auth test ran too early

The first time, I applied the auth policy right after SPIRE came up, and the client timed out too. The operator registers identities in SPIRE on its own schedule, and at that moment podinfo's entry did not exist yet (Found 0 entries). Three minutes later the same test passed. Check the SPIRE entry before blaming the policy.

One practical note: the kind node image has no tcpdump. The lab runs the capture from a nicolaka/netshoot pod with hostNetwork, pinned to the node, which sees the same eth0.

When to use it, and when not

Situation Choice
Cilium is already your network plugin and you want encryption between nodes, HTTP visibility and simple splits Cilium's sidecar-less mesh. No new components per pod.
You need retries, timeouts, fault injection or outlier detection every day Istio ambient (also sidecar-less: ztunnel per node, waypoint proxy for L7).
Regulation requires a certificate inside the application (end-to-end mTLS) A mesh with application-level mTLS; Cilium encrypts at the node.
You need service identity long term Plan for ztunnel; Cilium's SPIFFE mutual auth is deprecated in 1.20 and removed in 1.21.

Summary

  • A sidecar-less service mesh does encryption, identity, visibility and traffic management once per node, not once per pod.
  • With Cilium, WireGuard took readable HTTP on the wire from 6 lines to 0, in 31 seconds, with no change to the apps.
  • SPIFFE mutual auth blocked a pod without permission, at a cost of about one second for the first request per identity, and it is deprecated in 1.20.
  • An L7 rule gave HTTP visibility in Hubble, and GAMMA split in-cluster traffic 46/4 at 90/10.
  • Always check the agents after helm upgrade: the setting can say on while encryption is off.

Sources: Cilium docs Transparent Encryption (WireGuard), Mutual Authentication (Beta), Layer 7 Protocol Visibility and Gateway API GAMMA support; Cilium 1.20 upgrade notes; cilium/cilium#40533 and #47132; Gateway API GAMMA initiative.

Next in the series

This is the fifth post in the Cilium End to End series. Next: Cilium Cluster Mesh: Two kind Clusters, Global Services and Cross-Cluster Policy. Previous post: What Is Cilium Gateway API? What It Solves, What You Gain, and a Lab Without a Separate Ingress Controller.

The code for this lab (kind config, the mesh and encryption values with the SPIRE fix, the podinfo/client/intruder manifest, the two policies, the two GAMMA routes, the capture and auth scripts and the Makefile) is shared with Lab Access members on Buy Me a Coffee; the repo invite comes by email after you join: https://buymeacoffee.com/miraccanyilmaz/membership

İnsanın en büyük hatalarından biri de doğru zamanı yanlış kişilerle doldurmaktır.
Charles Bukowski