Skip to main content
RunBook Academy

KubernetesXLVI · Packet Capture in KubernetesPacket capture

Service mesh capture — Istio, Linkerd, Cilium, and the mTLS rewrites

Advanced⏱ ~17 minkubectltcpdumpistioctl

What you'll learn

  • Describe how Istio, Linkerd, and Cilium service mesh rewrite traffic
  • Identify why mTLS makes packet capture at L4 unhelpful
  • Apply the production patterns for capturing service mesh traffic
  • Distinguish plaintext capture from decrypted capture and the tooling for each

Prerequisites

Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16

Not yet marked complete on this device.

A service mesh adds a sidecar (Envoy, linkerd-proxy, or Cilium’s envoy) to every Pod. The sidecar intercepts all inbound and outbound traffic, applies L7 routing rules, and encrypts the byte stream with mTLS. The application sees a clean TCP connection to the upstream; the wire sees encrypted traffic between sidecars. This lesson walks through what capture sees at each layer of the mesh.

How a service mesh rewrites traffic

The standard mesh sidecar pattern:

flowchart LR
    A[app container] -->|localhost:15001| B[sidecar]
    B -->|mTLS to upstream| C[network]
    C --> D[upstream sidecar]
    D -->|localhost:8080| E[upstream app]

The application sends plaintext to localhost:15001 (Istio) or localhost:4191 (Linkerd). The sidecar intercepts via iptables (or eBPF for Cilium), establishes an mTLS connection to the upstream sidecar, and forwards the plaintext to the upstream application.

flowchart TB
    subgraph "Pod A"
      App1[app] -->|plaintext| S1[sidecar]
    end
    subgraph "Pod B"
      S2[sidecar] -->|plaintext| App2[app]
    end
    S1 -->|mTLS ciphertext| Net[underlay]
    Net --> S2

From the application’s perspective, the upstream is upstream-svc.namespace.svc.cluster.local:8080. From the wire’s perspective, the connection is between two Pod IPs, encrypted.

What packet capture sees at each layer

Capture pointWhat you see
Inside the app containerPlaintext HTTP/gRPC to localhost:15001. Useful for “is the app sending the right bytes?”
On the sidecar’s loopbackPlaintext HTTP/gRPC between app and sidecar. The sidecar sees what the app sent.
On the Pod’s eth0 (inter-sidecar)Encrypted mTLS ciphertext between Pod A and Pod B. Indistinguishable from random bytes.
On the node’s eth0 (overlay)Encrypted mTLS, then VXLAN/IPIP encapsulation. Double-encrypted.

The production rule: capture at L7 (the sidecar’s access log) for protocol-level debugging, not at L4 (the wire) for mesh traffic.

Istio capture patterns

Istio exposes the sidecar’s state through istioctl and Envoy’s admin interface. The production patterns:

# Substitute your own values before running:
POD=checkout-7d9f6b58c4-mn2wq
NS=production

# Inspect the sidecar's configuration for a Pod
istioctl proxy-config routes "$POD.$NS"
istioctl proxy-config clusters "$POD.$NS"
istioctl proxy-config endpoints "$POD.$NS"
istioctl proxy-config listeners "$POD.$NS"

These commands show what the sidecar knows: the routes, the upstream clusters, the endpoints, the listeners. A sidecar that does not have the right route is a configuration bug, not a network bug.

# Substitute your own values before running:
POD=checkout-7d9f6b58c4-mn2wq
NS=production

# Capture mesh traffic via the sidecar's tap
istioctl dashboard controlz "$POD.$NS"

Envoy’s controlz is the runtime admin UI. The dashboard shows the active connections, the cluster stats, and the listener configuration.

For packet-level capture, Istio’s tcpdump works but the output is ciphertext. The mesh’s value is in the L7 access logs, not the wire capture.

# Enable Istio access logs
kubectl apply -f - <<EOF
apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
  name: mesh-default
  namespace: istio-system
spec:
  accessLogging:
  - providers:
    - name: envoy
    disabled: false
EOF

The access log shows the request method, path, response code, source workload, destination workload, and the mTLS peer identity. This is what most “the mesh is dropping traffic” investigations need.

Linkerd capture patterns

Linkerd exposes a similar set of tools:

# Substitute your own values before running:
DEPLOY=checkout
PEER_DEPLOY=payments
NS=production

# Tap a workload's traffic
linkerd viz tap "deploy/$DEPLOY" -n "$NS" --to "deploy/$PEER_DEPLOY"

# Top routes by request rate
linkerd viz routes "deploy/$DEPLOY" -n "$NS"

# Live requests
linkerd viz stat "deploy/$DEPLOY" -n "$NS" --to "deploy/$PEER_DEPLOY"

Linkerd’s tap is closer to “capture what the application is sending” than Istio’s proxy-config, because Linkerd’s proxy emits a structured event per request.

# Substitute your own values before running:
DEPLOY=checkout
NS=production

# Live TCP-level capture (still mTLS ciphertext)
linkerd viz tap "deploy/$DEPLOY" -n "$NS" --method tcp

Linkerd’s mTLS is opportunistic. By default, Linkerd upgrades plaintext to mTLS when both ends have a linkerd-proxy. The tap command can capture both plaintext (before encryption) and the post-encryption metadata.

Cilium service mesh capture

Cilium uses eBPF, which means the sidecar is partly in the kernel. The capture patterns are different:

# Substitute your own values before running:
NS=production
POD=checkout-7d9f6b58c4-mn2wq

# Hubble observes all traffic (eBPF-based)
hubble observe --namespace "$NS" --pod "$POD"

# Hubble's flow logs (L7 visibility)
hubble observe --namespace "$NS" --follow --protocol http

Hubble is the kernel-level observability for Cilium. It sees the L3/L4 traffic as eBPF events and the L7 traffic (by Envoy integration) as structured HTTP/gRPC events. This is more comprehensive than sidecar capture because it covers Pod-to-Pod without sidecars.

# Substitute your own value before running:
NS=production

# Capture at L7 via Cilium's policy verdicts
hubble observe --verdict DROPPED --namespace "$NS"

hubble observe --verdict DROPPED is the production default for “the mesh is dropping traffic” — it shows exactly which flows the policy engine rejected.

mTLS and decryption

Production meshes use mTLS by default (Istio STRICT mode, Linkerd always). The wire is encrypted; the sidecar decrypts. To capture plaintext at the application, capture on the loopback interface inside the Pod:

# Inside the application Pod, on the loopback interface
tcpdump -ni lo port 15001 -w /tmp/app-to-sidecar.pcap

# On the Pod's eth0 (between sidecar and sidecar)
tcpdump -ni eth0 -w /tmp/sidecar-to-sidecar.pcap

The first capture is plaintext (app-to-sidecar). The second is mTLS ciphertext. The first is what the application sent; the second is what the wire carried.

The Istio and Linkerd access logs are the production-grade replacement for both: they show the L7 information without capturing bytes.

Quiz

Knowledge check · 4 questions

  1. Q1. An operator runs `tcpdump -ni eth0` on a Pod that is part of an Istio mesh with mTLS strict mode. What does the capture show?

  2. Q2. Cilium service mesh's Hubble can show which flows were dropped by the network policy at L7, including the policy name.

  3. Q3. An Istio mesh reports that requests from app-a to svc-b are returning 503s. Walk through the diagnostic ladder and explain what you capture at each layer.

    Pods in mesh with mTLS strict mode. Service svc-b has 3 endpoints, all healthy. The 503s are intermittent. The Envoy access log is enabled.

  4. Q4. Explain why a wire-level packet capture is not useful for debugging mTLS-encrypted service mesh traffic, and what to use instead.

Passing score: 75%. Answers are checked in this browser.

Production discipline

  • Default to the mesh’s L7 tooling. istioctl proxy-config, linkerd viz tap, hubble observe. These are designed for the mesh’s debugging questions.
  • Wire-level capture is a last resort. mTLS means the wire is ciphertext. Use the access log; use the policy verdict log; use the trace ID.
  • Capture plaintext on the loopback inside the Pod. tcpdump -ni lo port 15001 shows what the application sent to the sidecar.
  • Capture policy verdicts for “the mesh is dropping”. hubble observe --verdict DROPPED (Cilium) or Envoy access log with response_flags (Istio) shows the decision.
  • Treat the mesh’s metrics as the first signal. Request rate, error rate, latency (the RED method) tells you that there is a problem before you start asking what the problem is.