The CKS curriculum’s Minimize Microservice Vulnerabilities domain (20% of the exam) ends with a bullet that was rewritten in the 2024 refresh: “Implement Pod-to-Pod encryption (Cilium, Istio)”. The Istio half is the service-mesh mTLS story, and our Minimize Microservice Vulnerabilities guide already covers PeerAuthentication and STRICT mode. The Cilium half is different in kind: no sidecars, no application certificates, no change to a single Pod spec. Cilium encrypts the traffic between nodes at the network layer with WireGuard or IPsec, and the only thing a workload notices is nothing at all.
That property makes it the fastest way to satisfy “encrypt traffic between Pods” on a live exam cluster, which is exactly why it is worth understanding properly: what it encrypts, what it silently does not, how to turn it on without an internet connection, how keys are managed, and how to prove to yourself (and a grader) that packets on the wire are ciphertext. The current exam runs on Kubernetes v1.35 and Cilium’s documentation now targets the 1.20 series; everything below is checked against both.
Why Encrypt Pod Traffic at All
By default, a Kubernetes cluster is a flat network. Packets between Pods on different nodes cross whatever links connect those nodes: a cloud VPC, a rack switch, an overlay tunnel (VXLAN or Geneve) that adds a header but no confidentiality. Anyone with a tap on that path, a compromised node, or a misconfigured network device can read the traffic. HTTP service-to-service calls, database connections and gRPC streams carrying tokens all travel in clear text unless the application itself uses TLS.
Three different controls address three different questions, and the exam expects you to keep them apart:
| Control | Question it answers | Layer | Changes to the Pod |
|---|---|---|---|
| NetworkPolicy | May A talk to B at all? | L3/L4 policy in the CNI | none |
| Cilium transparent encryption | Is the traffic between nodes confidential on the wire? | L3 (IPsec ESP or WireGuard UDP tunnel) | none |
| Service-mesh mTLS (Istio) | Is each end who it claims to be, and is the connection encrypted end to end? | L7 proxy (sidecar or ambient) | proxy injection or ambient enrollment |
| Cilium mutual authentication (beta) | Is the peer identity verified via SPIFFE before policy allows the flow? | policy-plane handshake | none |
Transparent encryption does not authenticate workloads to each other and does not replace NetworkPolicy. A default-deny policy still decides who may connect, and encryption makes the allowed connections unreadable in transit. The CKS NetworkPolicy guide owns the first control; this article is about the second, with the third and fourth positioned where they intersect.
Cilium’s Three Encryption Modes
Cilium supports transparent encryption of traffic between Cilium-managed endpoints, and of Cilium-managed host traffic, using one of three mechanisms:
| Mode | Mechanism | Keys | Maturity | Pick it when |
|---|---|---|---|---|
| WireGuard | kernel WireGuard device cilium_wg0, UDP port 51871 | per-node key pairs generated automatically; public keys published on the CiliumNode object | stable | you want zero key management and a kernel ≥ 5.6 (or the out-of-tree module) |
| IPsec | kernel XFRM, ESP packets | a shared pre-shared key you create in a Secret, rotated manually by SPI | stable; encryption.type defaults to ipsec | compliance requires a specific algorithm (e.g. AES-GCM) or auditable key rotation |
| ztunnel | per-node proxy speaking mTLS (HBONE, port 15008) between enrolled namespaces | bootstrap and CA key pairs in Secrets; ephemeral client certificates | beta | you want identity-bearing mTLS without sidecars; TCP only |
For the exam, WireGuard and IPsec are the two that matter. ztunnel is worth recognising so a question about “a per-node proxy enrolled by the io.cilium/mtls-enabled=true namespace label” does not look like a trick.
What Is Encrypted, and What Is Not
This is where candidates lose points, because the mental model “encryption is on, therefore everything is encrypted” is wrong in five specific ways:
- Same-node Pod traffic is never encrypted. Two Pods on the same node talk through the node’s own kernel; there is no wire to protect. Cilium documents this as intended behaviour for both WireGuard and IPsec. If a question asks how to encrypt traffic between two Pods and the answer requires them to be co-located, transparent encryption is not it.
- Only Pod-to-Pod traffic is encrypted by default. Node-to-node, Pod-to-node and node-to-Pod traffic (health checks, host-networked Pods, kubelet-to-Pod probes) is not covered unless you also enable node encryption with
encryption.nodeEncryption=true, which is beta for WireGuard. Even then, nodes carrying thenode-role.kubernetes.io/control-planelabel are opted out automatically; thenode-encryption-opt-out-labelsoption changes that selector, and an empty selector forces control-plane encryption, which the documentation does not recommend. - There is an unencrypted first-packet window. Cilium decides whether to encrypt a packet with the same machinery it uses for policy: it must know the destination IP belongs to a remote Cilium-managed endpoint. When a new Pod or node appears and a sender is allowed to reach addresses outside the cluster, the first packets can leave in clear text until the endpoint information propagates. Egress strict mode (below) exists to close that window.
- Traffic to and from outside the cluster is not encrypted. Client-to-node traffic from external sources, north-south load-balancer traffic redirected between nodes with XDP acceleration or DSR in non-Geneve mode, and Egress Gateway replies with XDP enabled all bypass the tunnel.
- L7 proxy traffic is encrypted, but with caveats. In the default WireGuard mode, traffic that passes through Cilium’s Envoy proxy for L7 policy or Ingress is encrypted between Pods. With IPsec, enabling the L7 proxy requires
--dnsproxy-enable-transparent-mode=true, and Host Policies are not supported at all.
A concise rule for the exam: Cilium transparent encryption protects Pod-to-Pod traffic that crosses a node boundary; everything else needs an explicit extra setting or a different control.
Enabling WireGuard Encryption
Prerequisites
- Linux kernel 5.6 or newer with
CONFIG_WIREGUARD=m, or the out-of-tree WireGuard module on older kernels. - UDP port 51871 open between every pair of nodes. A firewall that blocks it produces a cluster where encryption is “enabled” and no cross-node Pod traffic flows.
- With CNI chaining (Cilium on top of another CNI such as the AWS VPC CNI), set
cni.enableRouteMTUForCNIChaining=trueto avoid fragmentation.
With Helm or the Cilium CLI
On a fresh install:
cilium install --version 1.20.3 \
--set encryption.enabled=true \
--set encryption.type=wireguard
On an existing Helm-managed installation, keep the other values and add encryption:
helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values \
--set encryption.enabled=true \
--set encryption.type=wireguard
Add --set encryption.nodeEncryption=true to either command to extend coverage to node traffic.
Without Helm: the ConfigMap route (exam-safe)
Exam clusters usually have no route to the public Helm repository, and you will not always know how Cilium was installed. The Cilium agent reads its settings from the cilium-config ConfigMap in kube-system, and the documented manual method is to set the key and restart the agents:
kubectl -n kube-system get cm cilium-config -o yaml | grep -iE 'wireguard|ipsec|encrypt'
kubectl -n kube-system patch cm cilium-config --type merge -p '{"data":{"enable-wireguard":"true"}}'
kubectl -n kube-system rollout restart ds/cilium
kubectl -n kube-system rollout status ds/cilium
The restart is mandatory; the agent reads the ConfigMap at start-up, not continuously. Expect a short disruption to cross-node connectivity while the DaemonSet rolls, and warn yourself before doing this on a cluster that is also running your other exam tasks.
Verifying WireGuard
The agent’s own status line is the fastest check. Run it from any Cilium Pod:
kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep Encryption
# Encryption: Wireguard [NodeEncryption: Disabled, cilium_wg0 (Pubkey: ..., Port: 51871, Peers: 2)]
Read the line carefully: the device is cilium_wg0, the port is 51871, and Peers should equal the number of nodes minus one. A peer count that is too low means one node has not published its key or its key does not match. Each node’s public key is published on its CiliumNode object:
kubectl get ciliumnodes -o custom-columns='NODE:.metadata.name,WGKEY:.metadata.annotations.network\.cilium\.io/wg-pub-key'
For per-peer detail, including allowed IPs and the last handshake time, use the agent’s debug output:
kubectl -n kube-system exec ds/cilium -- cilium-dbg debuginfo --output json | jq .encryption
For a remote peer to work, its public-key must match that node’s actual key and its allowed-ips must list the Pod IPs on that node. A missing allowed IP for a freshly scheduled Pod is the signature of the endpoint-propagation delay described earlier.
Finally, prove it on the wire. Inside a Cilium Pod, capture on the WireGuard device while generating cross-node traffic between two Pods:
kubectl -n kube-system exec -ti ds/cilium -- bash
apt-get update && apt-get -y install tcpdump
tcpdump -n -i cilium_wg0
Packets appearing on cilium_wg0 are packets entering or leaving the tunnel. If you capture on the node’s physical interface instead, you should see only UDP/51871 between node IPs and no readable Pod-to-Pod payload. Seeing plaintext HTTP between Pod IPs on eth0 while cilium_wg0 is quiet means the traffic is same-node, or one of the exceptions above applies.
Enabling IPsec Encryption
IPsec is the older of the two modes and the default value of encryption.type. The trade-off is that you own the key.
Create the key Secret first
The agent reads a pre-shared key from a Secret named cilium-ipsec-keys in the Cilium namespace (kube-system), under the data key keys. The value’s format is key-id encryption-algorithms PSK-in-hex key-size:
kubectl create -n kube-system secret generic cilium-ipsec-keys \
--from-literal=keys="3+ rfc4106(gcm(aes)) $(dd if=/dev/urandom count=20 bs=1 2>/dev/null | xxd -p -c 64) 128"
Decoding that line:
3is the key ID, also called the SPI. It is a uint8 in the range 1 to 15 and it is what you increment to rotate.+after the ID selects per-tunnel keys: Cilium derives a distinct key per node pair from the shared secret. The documentation calls the+strongly recommended, and plain global keys were deprecated in v1.16.rfc4106(gcm(aes))is AES-GCM via the Linux kernel’s crypto API; any algorithm the kernel supports can be used, but changing to an algorithm with a different authentication key length is not recommended because the agent delays the new key until restart.- The 20 random bytes become the hex key and
128is the key size in bits.
The Cilium CLI can produce the same line with cilium encrypt create-key --auth-algo rfc4106-gcm-aes.
Enable it
cilium install --version 1.20.3 \
--set encryption.enabled=true \
--set encryption.type=ipsec
or, on an existing install, helm upgrade ... --reuse-values --set encryption.enabled=true --set encryption.type=ipsec. In native (direct) routing mode you must also tell Cilium the native routing CIDR (--set ipv4NativeRoutingCIDR=<pod-cidr>), and --set encryption.ipsec.interface=ethX pins the interface if the default-route link is wrong.
Unlike WireGuard, IPsec is awkward to switch on through the ConfigMap alone: the agent Pod needs the cilium-ipsec-keys Secret mounted and the ipsec-key-file path configured, which the Helm chart only renders when IPsec is enabled. If an exam task hands you a Helm-managed Cilium and asks for IPsec, use helm upgrade with values; if it leaves the mode to you, WireGuard is the lower-risk choice.
Verifying IPsec
The in-agent status shows mode, decryption interfaces, keys in use, maximum sequence number and an error counter:
kubectl -n kube-system exec ds/cilium -- cilium-dbg encrypt status
Expect two keys per remote node per enabled IP family, doubling to four during a rotation. A non-zero error count is a kernel XFRM error and the first thing to investigate. On the wire, filter for ESP:
tcpdump -l -n -i eth0 esp # use cilium_vxlan instead of eth0 in tunnel routing mode
A capture on the same node shows both the outer ESP packet and the decrypted inner packet; that is normal, which is why you filter with esp to see only the encrypted ones.
Rotating the IPsec key
Rotation is a Secret update: read the current key ID, increment it (wrapping from 15 back to 1), and patch a new random key. During the transition both keys are in use, so no traffic is dropped:
KEYID=$(kubectl get secret -n kube-system cilium-ipsec-keys -o go-template --template={{.data.keys}} | base64 -d | grep -oP "^\d+")
if [[ $KEYID -ge 15 ]]; then KEYID=0; fi
data=$(echo "{\"stringData\":{\"keys\":\"$((($KEYID+1)))+ \"rfc4106(gcm(aes))\" $(dd if=/dev/urandom count=20 bs=1 2>/dev/null | xxd -p -c 64) 128\"}}")
kubectl patch secret -n kube-system cilium-ipsec-keys -p="${data}" -v=1
Three operational rules travel with that script: never rotate during a Cilium upgrade or downgrade (all nodes must run the same version first); in a Cluster Mesh, apply the rotation in every cluster and lengthen ipsec-key-rotation-duration if propagation needs more time; and expect agents to apply the new key with a random jitter of up to a tenth of the rotation duration to spread API server load.
WireGuard has no equivalent procedure. Each node generates its own static key pair, publishes the public half through the API server, and the WireGuard protocol itself renegotiates session keys continuously. A node that reboots or is re-provisioned simply publishes a new public key.
Closing the Gaps: Strict Mode
Two optional enforcement modes convert “encryption is enabled” into “unencrypted Pod traffic cannot happen”.
Egress strict mode (encryption.strictMode.egress.enabled=true with encryption.strictMode.egress.cidr=<pod-cidr>) enforces that traffic leaving a node toward the configured CIDR is always encrypted, which closes the first-packet window. The CIDR must be IPv4; IPv6 traffic is not protected by strict mode and can leak. If the node CIDR and Pod CIDR overlap or you are not in direct routing mode, encryption.strictMode.egress.allowRemoteNodeIdentities=true is required to permit unencrypted traffic to and from node identities.
Ingress strict mode (encryption.strictMode.ingress.enabled=true) drops any cluster-internal ingress traffic that did not arrive through the WireGuard tunnel, so an attacker on the network cannot inject plaintext Pod traffic toward local endpoints. It requires WireGuard (not supported with IPsec), works in both tunnel and native routing, does not cover node-to-node encryption, applies only to cluster-internal Pod traffic destined for local endpoints, and relies on Cilium’s BPF programs being attached to every interface that can carry Pod traffic. An unmanaged interface is a bypass. It is also not supported when chaining Cilium on top of another CNI.
In exam terms: “traffic to a brand-new Pod was observed unencrypted for a moment” is egress strict mode; “an attacker injected unencrypted packets toward Pods on a node” is ingress strict mode; and “we are on IPsec” rules the second one out.
Encryption Is Not Identity: Mutual Authentication and ztunnel
Transparent encryption guarantees confidentiality and integrity between nodes. It does not tell Pod B that the connection really came from Pod A rather than from any process on A’s node. Two Cilium features add identity, and both are beta:
- Mutual authentication uses SPIFFE identities issued by a SPIRE server (a central root of trust) and per-node SPIRE agents. It is enabled with
authentication.enabled=true,authentication.mutual.spire.enabled=trueandauthentication.mutual.spire.install.enabled=true, and is then requested per policy with anauthentication.mode: requiredstanza in aCiliumNetworkPolicy. The handshake happens out of band of the data connection, which means mutual authentication verifies identity but does not itself encrypt; the documentation tells you to enable WireGuard or IPsec alongside it. Cluster Mesh and external mTLS solutions are not compatible. - ztunnel enrolls whole namespaces (label
io.cilium/mtls-enabled=true) into a per-node proxy that establishes mTLS between enrolled workloads. Only TCP is covered, traffic between enrolled and non-enrolled workloads is unsupported, host-networked Pods are skipped, and L4 NetworkPolicies break for encrypted flows except those targeting the HBONE port 15008.
Contrast that with Istio, where mTLS provides encryption and workload identity and short-lived rotating certificates in one mechanism, at the cost of a proxy in the data path. When a question emphasises “authenticate the calling workload” or “short-lived workload certificates”, the answer is a mesh; when it emphasises “encrypt all Pod traffic without modifying workloads”, the answer is Cilium transparent encryption. The KCNA service mesh concepts guide explains the sidecar versus ambient models those questions reference.
Exam-Day Workflow
A realistic CKS task reads: “Cilium is installed in kube-system. Enable transparent encryption of Pod-to-Pod traffic using WireGuard and verify it is active.” A workflow that fits in a few minutes:
# 1. See how Cilium was installed and what is already set
kubectl -n kube-system get ds cilium -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
helm -n kube-system list 2>/dev/null
kubectl -n kube-system get cm cilium-config -o yaml | grep -iE 'enable-wireguard|enable-ipsec|encrypt'
# 2a. Helm-managed: upgrade with values
helm -n kube-system upgrade cilium cilium/cilium --reuse-values \
--set encryption.enabled=true --set encryption.type=wireguard
# 2b. Not Helm-managed or no repo access: flip the ConfigMap and restart
kubectl -n kube-system patch cm cilium-config --type merge -p '{"data":{"enable-wireguard":"true"}}'
kubectl -n kube-system rollout restart ds/cilium && kubectl -n kube-system rollout status ds/cilium
# 3. Verify
kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep -i encryption
kubectl get ciliumnodes -o custom-columns='NODE:.metadata.name,WGKEY:.metadata.annotations.network\.cilium\.io/wg-pub-key'
# 4. Prove it: two Pods on different nodes, capture on the tunnel device
kubectl run c1 --image=nginx --overrides='{"spec":{"nodeName":"node01"}}'
kubectl run c2 --image=busybox --overrides='{"spec":{"nodeName":"node02"}}' -- sleep 3600
kubectl exec c2 -- wget -qO- http://$(kubectl get pod c1 -o jsonpath='{.status.podIP}')
kubectl -n kube-system exec ds/cilium -- sh -c 'timeout 5 tcpdump -n -i cilium_wg0 2>/dev/null | head'
If tcpdump is not in the agent image and there is no package access, the status line and the peer count are sufficient evidence; note the peer count equals node count minus one.
Troubleshooting Table
| Symptom | Likely cause | Check |
|---|---|---|
Encryption: Disabled after editing the ConfigMap | agents not restarted | rollout restart ds/cilium |
| Peers count lower than nodes−1 | a node’s public key missing or stale | CiliumNode annotation; agent logs on that node |
| Cross-node Pod traffic stops after enabling WireGuard | UDP 51871 blocked | security group / firewall between nodes |
Plaintext seen between Pods on eth0 | same-node Pods, or traffic path exempt (XDP/DSR, external) | schedule on different nodes; check routing mode |
| Brief plaintext to a new Pod | endpoint propagation window | egress strict mode |
cilium-dbg encrypt status shows XFRM errors | IPsec kernel state problem, algorithm change | agent logs; avoid changing auth key length |
| Wrong number of IPsec keys | mid-rotation (4 expected) or a node missed the Secret | wait for ipsec-key-rotation-duration; check Secret mount |
| Pods throughput between two nodes caps unexpectedly on IPsec | decryption limited to one CPU core per tunnel | documented limitation; WireGuard scales differently |
| Packet drops while endpoints change | known WireGuard device reconfiguration issue | transient; verify after churn settles |
Common Mistakes That Cost Points
- Enabling encryption and not restarting the agents. The ConfigMap change does nothing until the DaemonSet rolls.
- “Verifying” with two Pods on the same node. Their traffic is never encrypted, so the capture shows plaintext and you conclude encryption is broken.
- Confusing node encryption with Pod encryption. Host-networked Pods, kubelet probes and node-to-node traffic need
encryption.nodeEncryption=true, and control-plane nodes opt out by default. - Creating the IPsec Secret with the wrong name, namespace or key. It must be
cilium-ipsec-keysin the Cilium namespace with data keykeys. - Forgetting the
+. Without it you get a single global key, a deprecated and weaker configuration. - Rotating the IPsec key during a Cilium upgrade. The documentation forbids it.
- Expecting ingress strict mode on IPsec. It requires WireGuard.
- Treating transparent encryption as mTLS. It encrypts; it does not authenticate workload identity. If the task says “mutual authentication”, you need SPIRE-backed mutual auth, ztunnel or a mesh.
- Dropping NetworkPolicy because traffic is encrypted. Encryption does not restrict who may connect.
Practice on a Real Cluster Before Exam Day
Transparent encryption is a task you cannot learn from a diagram: the proof is a status line, a peer count and a packet capture, and the failure modes (a missed restart, a blocked UDP port, two Pods that happened to land on the same node) only appear when you do it on a multi-node cluster with the clock running. The CKS mock exam bundle includes performance-based tasks across all six domains, including encryption and isolation scenarios in the Minimize Microservice Vulnerabilities domain, with step-by-step solutions that show the verification commands graders expect. Combine it with the CKS exam practice environment guide to build a two-node Cilium lab, the free CKS practice guide for low-cost cluster options, and the CKS study plan to schedule this alongside sandboxing and user namespaces, the other two isolation techniques in the same domain.
Frequently Asked Questions
What is Cilium transparent encryption?
It is Cilium’s ability to encrypt traffic between Cilium-managed endpoints on different nodes at the network layer, using WireGuard or IPsec (with a beta ztunnel mTLS option), without any change to Pods, sidecars or application code. Workloads keep sending plaintext; the node encrypts it before it reaches the wire and the receiving node decrypts it.
Does Cilium encrypt traffic between Pods on the same node?
No. Same-node Pod traffic never leaves the node and is not encrypted in either WireGuard or IPsec mode. This is documented as intended behaviour and is the most common reason a verification capture shows plaintext.
WireGuard or IPsec for the CKS?
WireGuard requires no key management (each node generates its own key pair and publishes the public key on its CiliumNode), can be enabled by setting enable-wireguard: "true" in the cilium-config ConfigMap and restarting the agents, and supports ingress strict mode. IPsec requires you to create and rotate the cilium-ipsec-keys Secret but lets you choose the algorithm. When the task does not dictate a mode, WireGuard is the lower-risk choice under time pressure.
How do I verify that Cilium encryption is working?
Run cilium-dbg status | grep Encryption inside a Cilium Pod and confirm the mode, the cilium_wg0 device on port 51871 (WireGuard) and a peer count equal to the number of nodes minus one. For IPsec, cilium-dbg encrypt status shows keys in use and an error counter. Then generate traffic between Pods on different nodes and capture on cilium_wg0, or filter for esp on the node interface for IPsec.
How are IPsec keys rotated in Cilium?
Patch the cilium-ipsec-keys Secret with a new line whose key ID (SPI) is incremented by one, wrapping from 15 back to 1, and a freshly generated random key. Agents pick up the new key with a short random jitter and keep both keys active during the transition, so no traffic is dropped. Do not rotate during a Cilium upgrade.
Does transparent encryption replace a service mesh’s mTLS?
No. Transparent encryption provides confidentiality between nodes but does not verify workload identity. Istio mTLS and Cilium’s beta mutual authentication (SPIFFE/SPIRE) or ztunnel add identity. For the CKS, “encrypt Pod traffic without touching workloads” points to Cilium; “authenticate the calling service with short-lived certificates” points to a mesh.
Is node-to-node traffic encrypted when I enable Cilium encryption?
Not by default. Enable encryption.nodeEncryption=true to cover node-to-node, Pod-to-node and node-to-Pod traffic. Control-plane nodes are excluded automatically unless you change node-encryption-opt-out-labels, and node-to-node encryption is beta for WireGuard and not protected by ingress strict mode.
Conclusion
The Cilium half of “Implement Pod-to-Pod encryption” is a network-layer control: WireGuard or IPsec tunnels between nodes that make cross-node Pod traffic unreadable on the wire without touching a single workload. Knowing how to enable it (enable-wireguard in the ConfigMap plus an agent restart, or the Helm values), where the keys live (a per-node CiliumNode annotation for WireGuard, the cilium-ipsec-keys Secret with its rotating SPI for IPsec), what it deliberately leaves out (same-node traffic, node traffic without node encryption, the first-packet window, external clients) and how to prove it is working (cilium-dbg status, the peer count, a capture on cilium_wg0 or an esp filter) covers both the exam task and the production deployment. Pair it with default-deny NetworkPolicy for who may talk and with mesh mTLS or SPIFFE-backed mutual authentication when who is talking matters too.
Continue with the Minimize Microservice Vulnerabilities guide for the Istio side of the same bullet and the rest of the domain, the container sandboxing guide for the isolation techniques that sit beside it, and the CKS exam guide for the current curriculum, Kubernetes version and exam logistics.