DEV Community

Amaresh Pelleti
Amaresh Pelleti

Posted on Originally published at devtoolhub.com

Cilium mTLS ztunnel: Sidecar-Free Encryption Explained

Originally published on DevToolHub.

Cilium mTLS ztunnel doesn't need a SPIRE server — and that's actually the second-biggest news here. The bigger story: Cilium's older, SPIRE-based "Mutual Authentication" feature is now officially deprecated, with removal targeted for v1.21. If you adopted it, you're migrating whether you planned to or not. I fetched the real docs and the maintainer's own deprecation issue to find out what that migration actually involves — and the real gotcha isn't SPIRE at all. (New to Cilium or Kubernetes networking generally? Start with our Kubernetes complete guide.)

What Changed: SPIRE-Based Mutual Auth Is Being Removed

Cilium shipped a SPIRE-based "Mutual Authentication" feature back in 2023 (as a Beta), requiring a SPIRE server deployed alongside Cilium (authentication.mutual.spire.enabled=true) to issue and verify identity certificates. Per the maintainer's own deprecation issue, opened by a Cilium committer: "Given the lack of progress on Mutual Auth, and the presence and at least equal stability of the new feature, as of Cilium v1.20, Cilium committers are marking the Mutual Auth feature as deprecated, and will remove the feature in a later version, most likely v1.21." The agent-side removal PR is already merged.

The replacement is ztunnel Transparent Encryption — also Beta, but actively developed, and it doesn't ask you to run SPIRE at all.

Cilium mTLS ztunnel: How It Actually Works (No SPIRE Needed)

Per Cilium's ztunnel documentation, ztunnel is "a purpose-built per-node proxy that provides transparent Layer 4 mTLS encryption and authentication for pod-to-pod communication." Instead of a SPIRE server, it needs a set of Kubernetes secrets generated with a short OpenSSL script the docs provide — a bootstrap key pair (secures the connection between ztunnel and Cilium's certificate server) and a CA key pair (the root for ephemeral, in-memory client certificates ztunnel issues on request):

openssl genrsa -out bootstrap-private.key 2048
# ... (bootstrap cert generation, see docs)
openssl genrsa -out ca-private.key 2048
# ... (CA cert generation, see docs)

kubectl --namespace kube-system create secret generic cilium-ztunnel-secrets \
  --from-file=bootstrap-private.key=bootstrap-private.key \
  --from-file=bootstrap-root.crt=bootstrap-root.crt \
  --from-file=ca-private.key=ca-private.key \
  --from-file=ca-root.crt=ca-root.crt
Enter fullscreen mode Exit fullscreen mode

Then enable it at install time:

cilium install 1.20.2 \
  --set encryption.enabled=true \
  --set encryption.type=ztunnel
Enter fullscreen mode Exit fullscreen mode

One hard precondition the docs call out: Cluster Mesh must be disabled first — ztunnel "is currently not compatible with Cluster Mesh," and enabling both together throws a validation error.

Enrolling Namespaces (Not Pods)

Once ztunnel is running, nothing is encrypted until you opt namespaces in. Per the docs, enrollment is a namespace label:

kubectl label namespace <namespace-name> io.cilium/mtls-enabled=true
Enter fullscreen mode Exit fullscreen mode

Every existing pod in that namespace gets enrolled (ztunnel pods excepted), iptables rules get installed in each pod's network namespace to redirect traffic to the local ztunnel proxy, and every future pod created in the namespace is enrolled automatically. Disenrolling is the same label, removed: kubectl label namespace <namespace-name> io.cilium/mtls-enabled-.

That namespace-level granularity sounds convenient for a gradual rollout. It's also where the real migration risk is.

The Real Migration Gotcha: Partial Enrollment Breaks Traffic

The docs are explicit about this, in one line easy to skim past: "Traffic between workloads is only supported when both the source and destination endpoints are enrolled in ztunnel. Communication between an enrolled workload and a non-enrolled workload is not supported."

In practice: if you enroll payments but haven't gotten to orders yet, and payments calls orders, that call breaks — not silently degrades to unencrypted, just doesn't work. A namespace-by-namespace rollout plan has to account for every namespace a given workload talks to, not just the one you're migrating. For anything beyond a small cluster, map your service-to-service dependencies before you label the first namespace, not after something stops responding.

The Bigger Gotcha: It Can Silently Break L4 NetworkPolicy

This is the one that catches people who already did their homework on the namespace-enrollment rule. Per the docs: "ztunnel interferes with Cilium network policy as traffic is encrypted before it leaves the pod, meaning L4 policies won't work except for directly targeting the ztunnel HBONE port (15008)."

If you're running Kubernetes NetworkPolicies or Cilium-native L4 policies to restrict which pods can talk to which ports, enabling ztunnel on a namespace can quietly stop those policies from doing their job — because by the time your policy would evaluate the traffic, it's already encrypted and tunneled through port 15008. This doesn't throw an error. It just means an L4 rule you're relying on may no longer enforce what you think it does. Re-verify your NetworkPolicy behavior after enrolling a namespace, not just your mTLS connectivity.

Cilium mTLS ztunnel: Other Real Limitations (Confirmed in the Docs)

  • TCP only. UDP and other protocols aren't redirected through ztunnel — if a service depends on UDP, it isn't covered by this encryption.
  • Namespace-level enrollment only. No per-pod opt-in; everything in an enrolled namespace is in.
  • Host-networked pods are skipped. No network namespace path means no enrollment — they won't error, they'll just stay unencrypted.
  • Requires iptables support in the kernel. Minimal container runtimes without iptables can't use it.

mTLS secures what's in transit between pods; it doesn't tell you what a compromised pod does once it's running. Pair it with runtime visibility — see our Falco on Kubernetes guide for the detection side of that equation.

Frequently Asked Questions

Q: Do I need to run a SPIRE server for Cilium's native mTLS?
A: No. ztunnel generates and manages its own certificates from a bootstrap/CA key pair you create once via a provided OpenSSL script — no external PKI or SPIRE deployment required.

Q: Is Cilium's SPIRE-based Mutual Authentication feature going away?
A: Yes. Per the Cilium maintainers' own deprecation issue, it was marked deprecated as of v1.20, with removal "most likely" in v1.21. The agent-side removal code is already merged.

Q: Can I migrate one namespace at a time?
A: Technically yes via the namespace label, but only if every namespace that workload talks to is migrated together — traffic between an enrolled and a non-enrolled workload isn't supported.

Q: Will my existing NetworkPolicies still work after enabling ztunnel?
A: Not necessarily. L4 policies stop enforcing as expected once ztunnel encrypts traffic before it leaves the pod, except for rules that directly target ztunnel's HBONE port (15008). Re-test policy behavior after enrollment.

Q: What Cilium version is this in?
A: ztunnel Transparent Encryption is Beta as of Cilium 1.20.2, the current stable release at time of writing.

Quick Summary:

  • Cilium's older SPIRE-based Mutual Authentication is deprecated as of v1.20 and slated for removal in v1.21 — this isn't optional for long
  • The replacement, ztunnel Transparent Encryption, needs no SPIRE server — it generates its own bootstrap/CA certs via a provided script
  • Enrollment is per-namespace (io.cilium/mtls-enabled=true), not per-pod
  • Real gotcha #1: traffic between an enrolled and a non-enrolled workload isn't supported — map dependencies before migrating
  • Real gotcha #2: L4 NetworkPolicy can silently stop enforcing once ztunnel encrypts traffic, except for rules targeting port 15008
  • Cluster Mesh is incompatible with ztunnel and must be disabled first; only TCP is covered, not UDP

Cilium mTLS ztunnel is where this migration ends up either way: if you're still running the SPIRE-based mutual auth, the removal timeline makes it a when, not an if — plan the namespace rollout around your actual service dependency graph, and re-test NetworkPolicy behavior before you call it done.

Top comments (0)