← Writeups
CiliummTLSService MesheBPF

Cilium m!TLS: The mTLS That Wasn't

We left Istio's sidecar mesh for Cilium and assumed we kept mTLS along the way. We did not. The feature branded "mTLS-based" is mutual authentication, not encryption, and the difference is the whole point of mTLS. Here is how the name fooled us, why the maintainers agreed it was wrong, and why the real fix is still out of reach.

The title is the finding: what Cilium sold us as mTLS is m-not-TLS. When we migrated our dataplane off Istio's sidecar mesh onto Cilium eBPF, one line item on the checklist was mTLS. Istio gave us mutual TLS between every pod for free, and the Cilium docs advertised an "mTLS-based" Mutual Authentication feature, so we ticked the box and moved on. Months later, reading more carefully, we realized the box should never have been ticked. This is what mTLS actually promises, what Cilium's feature actually delivers, and the gap we walked straight into.

What we thought we were getting

mTLS is supposed to mean two things at once, on every connection: both sides prove who they are, and the traffic between them is encrypted in a session only they can read. Authenticated and encrypted, per connection. That is the guarantee Istio's sidecars gave us, and it is the guarantee the word "mTLS" carries in every other context.

So when the Cilium docs described Mutual Authentication as "mTLS-based," we heard the whole promise. We should have read which half of it was actually on offer. Click through each option and watch which guarantees survive:

What does each option actually guarantee?

"mTLS" is supposed to mean two things at once: authenticated and encrypted, per connection. Click each option and watch which of those it really gives you.

Cilium Mutual Authentication
Mutual auth
Encrypted
Per-connection

What the docs called "mTLS-based." It verifies identity with a SPIFFE handshake, run out-of-band by an auth agent and SPIRE. On its own it does not encrypt the traffic at all. Authentication is not encryption.

Only the top and bottom rows are actually mTLS. The one the docs sold as "mTLS-based" is the second one, authenticated but not encrypted. That is the gap we walked into.

The feature that reads as "mTLS-based" is the one that authenticates but does not encrypt. Only the first and last rows are actually mTLS.

Authentication is not encryption

Here is the mechanism, because the mechanism is the whole trick. Cilium Mutual Authentication runs a SPIFFE identity handshake out of band: a dedicated auth agent, backed by SPIRE, performs a mutual TLS handshake between the two identities and issues short-lived X.509 SVIDs. That handshake proves who the endpoints are. Then it steps aside. The application traffic itself flows over the normal Cilium datapath, and unless you have separately turned on transparent encryption, it flows in plaintext.

To be clear, I am not knocking the approach. A SPIFFE/SPIRE identity handshake, run out of band, is a genuinely secure way to do authentication, and it is a big part of why this was never a five-alarm dealbreaker for us: the endpoints really do prove who they are, cryptographically, with short-lived identities. My objection is narrower. It simply is not encryption. And a feature that does the first thing while its name implies the second is misleading, however good that first half is.

That is the sleight of hand. There is a mutual TLS handshake in there, so "mTLS-based" is not technically a lie, but it happens beside the connection, not on it. It authenticates the wire without protecting it. Toggle between what we assumed and what mTLS actually is:

The handshake proves who. It does not protect the wire.

This is the part the name hides. Toggle between what we thought we had and what mTLS actually is.

Out-of-band: identity handshakeauth agent + SPIRE do a mutual TLS handshake, once, just to check SPIFFE identities
Pod A
application traffic: plaintextunless you separately turn on WireGuard or IPsec, and even then it is node-to-node, not this session
Pod B

In Cilium Mutual Authentication they are split apart. The handshake authenticates; the data path is only encrypted if you wired up transparent encryption yourself. "mTLS-based" quietly implied the second half.

The handshake proves identity out of band. In real mTLS the same session that proves identity also encrypts the data. Cilium splits the two apart.

The maintainers agreed the name was wrong

This is not just my reading. It was argued out in the open. In cilium/cilium issue 24867, a maintainer put it plainly: we should not call anything mTLS if we are not ensuring the traffic is encrypted end to end with a session-specific key. The team agreed, and PR 25509 renamed the feature from "mTLS" to "Mutual Authentication" and reframed it as an authentication primitive that composes with transparent encryption, rather than a thing that encrypts on its own.

So the docs today are honest: it is Mutual Authentication, expressed as an authentication mode on a network policy. The problem is that the earlier framing had already done its work on teams like mine. Here is what the feature really is, in config, and where the encryption actually has to come from:

yaml
# Mutual Authentication is an AUTH mode on a policy. It proves identity.
# It does not encrypt anything on its own.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: backend-requires-auth
spec:
  endpointSelector:
    matchLabels:
      app: backend
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: frontend
      authentication:
        mode: required        # SPIFFE/SPIRE handshake, out of band. Auth only.

# Encryption is a SEPARATE switch, node-to-node, with its own key.
# This is the half "mTLS-based" quietly implied you already had.
# cilium Helm values:
encryption:
  enabled: true
  type: wireguard             # or ipsec, either way, NOT per-connection TLS

What we actually run today

Once we understood the split, we did the responsible thing and turned on the other half: transparent encryption with WireGuard, alongside Mutual Authentication. So our traffic is authenticated and it is encrypted, which is a genuinely fine security posture and better than a lot of clusters run.

But it is still not mTLS, and it is worth being precise about why. WireGuard and IPsec encrypt node-to-node, with a shared key between nodes, not per-connection with a session key negotiated by the two endpoints. It protects the wire, but it is a different shape of guarantee than the per-connection mutual TLS session Istio gave us. We had traded a single feature that did both for two features that, stacked, approximate it.

The real fix arrived, and we can't take it yet

Then, at KubeCon EU 2026, the actual thing landed. Cilium announced Native mTLS: real, per-connection mutual TLS, transparent, no sidecars, built on a per-node ztunnel proxy speaking HBONE, the same mechanism Istio's ambient mode uses. Authenticated and encrypted in one session, per connection. Exactly what we wanted the first time.

And we still can't adopt it. It is Beta, which alone would keep it off our critical path for a security guarantee. But the hard blocker is in the Known Issues: Cluster Mesh is not supported when ztunnel is enabled, and enabling both fails validation. We run Cluster Mesh specifically to reach a legacy OKE cluster we migrated onto from Istio Multi-Cluster, so giving it up is not on the table. Click through what ztunnel delivers, and what stands in our way:

The fix arrived. We still can't take it.

KubeCon EU 2026 brought real Native mTLS to Cilium, via ztunnel. Here is what it delivers, and why it is not an option for us yet. Click a blocker.

What it finally delivers
  • Real per-connection mTLS: authenticated and encrypted, with session keys
  • Transparent, no sidecars: a per-node ztunnel proxy
  • Runs over HBONE, the same mechanism Istio ambient uses
Why we can't adopt it yet
No Cluster Mesh supportthe one that blocks us

The dealbreaker for us. The docs' Known Issues say Cluster Mesh is not supported when ztunnel is enabled. We migrated from Istio Multi-Cluster to Cilium Cluster Mesh specifically to reach a legacy OKE cluster, so giving up Cluster Mesh is not on the table.

Native mTLS via ztunnel is finally the real thing. For a cluster that depends on Cluster Mesh, it is also currently unusable.

The lesson

The engineering lesson here is not that Cilium did something wrong; the datapath, the eBPF foundation, and the eventual ztunnel design are all excellent, and the maintainers fixed the name themselves. The lesson is about how a feature name becomes a checklist item and then a false assumption. "mTLS-based" was close enough to "mTLS" that a busy migration checked the box, and no one re-read the guarantee until much later.

So now I read the guarantee, not the label. For anything security-critical I ask the boring, literal question: on this exact connection, is the traffic authenticated, and is it encrypted, and with whose key? If the answer has an "unless you also turn on…" in it, then the label is doing more work than the feature. That is the whole story of Cilium m!TLS: a missing exclamation mark's worth of encryption, hiding inside a name that read like a promise.

The migration this came from: Istio sidecars to Cilium eBPFCompanion writeup: Falco to TetragonMutual Authentication (docs)Native mTLS for Cilium with ztunnel (blog)Ztunnel Transparent Encryption, Beta (docs)