🔐 Why Encrypt East-West Traffic?

NetworkPolicies control which pods can talk to each other — but they don't protect the content of that traffic. A compromised node or a MITM on the pod network can read unencrypted service-to-service calls. Mutual TLS (mTLS) solves this by requiring both sides to present a certificate, so:

  • Confidentiality — traffic is encrypted, no eavesdropping
  • Integrity — tampering is detected
  • Mutual authentication — both client and server prove their identity
⚠️ The zero-trust assumption In a zero-trust model you assume the network is hostile — even inside the cluster. A rogue pod on the same node should not be able to impersonate payments-svc. mTLS with workload identity enforces this assumption.

mTLS Handshake in 4 Steps

Client sidecar proxy cert: SVID spiffe://cluster/ ns/checkout Server sidecar proxy cert: SVID spiffe://cluster/ ns/payments ① ClientHello + client certificate (SVID) ② ServerHello + server certificate (SVID) ③ Verify server cert → derive session keys ④ Encrypted application data ↔

🕸️ Service Meshes — mTLS at Scale

Implementing mTLS manually in every service (cert rotation, CA management, retries) is impractical. A service mesh automates this by injecting a sidecar proxy (Envoy, Linkerd-proxy) into every pod. All traffic is intercepted by the proxy — the application sees plain HTTP, the network sees mTLS.

Istio

Industry standard. Envoy sidecars, rich traffic management, fine-grained AuthorizationPolicy. Higher complexity and resource overhead.

Linkerd

Lightweight, Rust-based micro-proxy. Automatic mTLS with zero config. Lower overhead, less feature-rich than Istio. CNCF graduated.

Cilium Mesh

eBPF-based — no sidecar. mTLS enforced at the kernel level. Best performance, requires kernel 5.10+.

AWS App Mesh / GCP Traffic Director

Managed control planes that use Envoy sidecars. Good for cloud-native shops already in one provider's ecosystem.

Istio mTLS in Practice

# Enable strict mTLS for an entire namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: payments
spec:
  mtls:
    mode: STRICT   # reject any plaintext connection
# AuthorizationPolicy — only checkout SA can call payments
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: payments-authz
  namespace: payments
spec:
  action: ALLOW
  rules:
  - from:
    - source:
        principals:
        - "cluster.local/ns/checkout/sa/checkout-svc"
    to:
    - operation:
        methods: ["POST"]
        paths: ["/api/v1/charge"]
💡 PERMISSIVE before STRICT When rolling out Istio mTLS, use mode: PERMISSIVE first. This lets non-mesh services still connect while you migrate them. Switch to STRICT once all services in the namespace have sidecars injected.

Linkerd — Zero-Config mTLS

# Annotate a namespace for automatic sidecar injection + mTLS
kubectl annotate namespace payments linkerd.io/inject=enabled

# Verify mTLS is active between two pods
linkerd viz edges deployment -n payments

# Check identity of a specific pod
linkerd identity -n payments deploy/payment-processor

🚪 Egress Controls — Locking Down Outbound Traffic

Ingress controls (NetworkPolicy, Ingress/Gateway) are well understood, but egress is often overlooked. An attacker who compromises a pod needs to exfiltrate data or beacon to a C2 server — both require outbound connectivity. Controlling egress limits what a compromised workload can do.

Layer 1: NetworkPolicy Egress Rules

The simplest egress control — already covered in the networking chapter, but worth a reminder in the security context. A default-deny egress policy:

# Default-deny all egress in a namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: payments
spec:
  podSelector: {}
  policyTypes: [Egress]

---
# Allow only DNS + specific external CIDR
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: payments-allowed-egress
  namespace: payments
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
  - ports:                    # allow DNS
    - port: 53
      protocol: UDP
  - to:                       # allow stripe API
    - ipBlock:
        cidr: 54.187.174.169/32
    ports:
    - port: 443
⚠️ NetworkPolicy works at L3/L4 only NetworkPolicy can restrict by IP and port, but it can't do hostname-based filtering (e.g. "allow *.stripe.com but deny everything else"). For that you need a dedicated egress gateway or DNS-aware proxy.

Layer 2: Istio Egress Gateway

Istio provides an Egress Gateway — a dedicated Envoy proxy that all outbound traffic is forced through. You can then apply fine-grained L7 rules, TLS origination, and audit logging:

# Define an external service
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  name: stripe-api
spec:
  hosts: [api.stripe.com]
  ports:
  - number: 443
    name: https
    protocol: HTTPS
  resolution: DNS
  location: MESH_EXTERNAL

---
# Block all other external traffic via Sidecar resource
apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
  name: restrict-egress
  namespace: payments
spec:
  egress:
  - hosts:
    - "payments/*"         # own namespace services
    - "istio-system/*"     # telemetry
    - "./stripe-api"       # external Stripe only

Layer 3: Cilium FQDN-Based Egress Policy

Cilium's DNS-aware network policies allow egress rules based on fully-qualified domain names — much more practical than managing IP CIDRs for external APIs:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: payments-fqdn-egress
  namespace: payments
spec:
  endpointSelector:
    matchLabels:
      app: payment-processor
  egress:
  - toFQDNs:
    - matchName: "api.stripe.com"
    - matchPattern: "*.stripe.com"
    toPorts:
    - ports:
      - port: "443"
        protocol: TCP
  - toEndpoints:               # allow kube-dns
    - matchLabels:
        k8s:io.kubernetes.pod.namespace: kube-system
        k8s:k8s-app: kube-dns

Egress Controls Comparison

ApproachGranularityHostname supportL7 visibilityWhen to use
NetworkPolicy egressL3/L4 (IP+port)NoNoSimple IP allowlists, no mesh
Istio Egress GatewayL7 (hostname, path)YesYesFull mesh already deployed
Cilium FQDN policyL3/L4 + DNS nameYes (DNS-aware)PartialCilium CNI, no sidecar overhead
HTTP proxy (Squid etc)L7 (URL/hostname)YesYesLegacy on-prem, compliance

🪪 SPIFFE & SPIRE — Workload Identity

SPIFFE (Secure Production Identity Framework For Everyone) is a CNCF standard that defines how workloads prove their identity using short-lived X.509 certificates called SVIDs (SPIFFE Verifiable Identity Documents). SPIRE is the reference implementation.

The SPIFFE Identity Format

Every workload gets a URI SAN of the form:

spiffe://<trust-domain>/<path>

# Examples:
spiffe://prod.example.com/ns/payments/sa/payment-processor
spiffe://prod.example.com/ns/checkout/sa/checkout-svc
spiffe://staging.example.com/k8s/cluster1/ns/default/pod/my-pod

The trust domain is your cluster/org identifier. The path encodes the workload's namespace and ServiceAccount. This identity is cryptographically verifiable — it's in the X.509 cert's SAN field, signed by SPIRE's CA.

SPIRE Architecture

SPIRE Server CA / signing authority registration entries JWT / X.509 SVID issuer Datastore (SQLite/Postgres) SPIRE Agent (DaemonSet — one per node) attests node identity issues SVIDs to workloads Workload API (Unix socket) Workload (Pod / container) calls Workload API receives X.509 SVID auto-rotated (1h TTL) attest SVID

Registering a Workload in SPIRE

# Create a registration entry for a K8s workload
spire-server entry create \
  -spiffeID spiffe://prod.example.com/ns/payments/sa/payment-processor \
  -parentID spiffe://prod.example.com/k8s-node/node1 \
  -selector k8s:ns:payments \
  -selector k8s:sa:payment-processor \
  -ttl 3600

# Workload fetches its SVID via the Unix socket
# (SPIRE agent mounts /run/spire/sockets/agent.sock into pods)
spire-agent api fetch x509 \
  -socketPath /run/spire/sockets/agent.sock

SPIFFE vs Kubernetes ServiceAccount Tokens

K8s ServiceAccount TokenSPIFFE SVID (SPIRE)
FormatJWT (bound token)X.509 certificate + optional JWT-SVID
TTL1 hour (projected), configurableConfigurable, typically 1 hour, auto-rotated
ScopeK8s API onlyAny system — cross-cluster, multi-cloud
Mutual authServer only (no client cert)Both sides prove identity (mTLS)
Cross-clusterNot nativelyYes — federated trust bundles
🔵 When do you need SPIRE? SPIRE is most valuable in multi-cluster, multi-cloud, or hybrid environments where you need a single identity plane across Kubernetes, VMs, and cloud services. For single-cluster mTLS, most service meshes (Istio, Linkerd) handle identity internally without needing to deploy SPIRE separately.

🧠 Knowledge Check

Q1. What does "mutual" mean in mutual TLS (mTLS)?

A) The connection is multiplexed over a single TCP stream
B) Both client and server present certificates, providing mutual authentication
C) Traffic is encrypted in both directions simultaneously
D) Two TLS sessions are established per connection for redundancy

Q2. An Istio PeerAuthentication has mtls.mode: PERMISSIVE. What does this mean?

A) Only mTLS connections are accepted; plaintext is rejected
B) Any workload may connect without a certificate
C) Both mTLS and plaintext connections are accepted — useful during migration
D) mTLS is disabled and all traffic is plaintext

Q3. A SPIFFE SVID has the URI: spiffe://prod.acme.com/ns/billing/sa/invoice-svc. What does each part tell you?

A) The hostname of the node running the pod
B) The URL of the SPIRE server API endpoint
C) Trust domain (prod.acme.com) + workload path (namespace billing, ServiceAccount invoice-svc) — a cryptographically verifiable workload identity
D) A Kubernetes RBAC role binding path

Q4. Your security team wants to allow pods in the payments namespace to reach api.stripe.com:443 but block all other outbound traffic. Which tool handles hostname-based egress most cleanly without a full service mesh?

A) Standard Kubernetes NetworkPolicy with an ipBlock CIDR rule
B) Istio AuthorizationPolicy with a hostname match
C) Cilium CiliumNetworkPolicy with toFQDNs — DNS-aware egress without a full mesh
D) A seccomp profile blocking the connect syscall