🔐 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
payments-svc. mTLS with workload identity enforces this assumption.
mTLS Handshake in 4 Steps
🕸️ 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"]
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
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
| Approach | Granularity | Hostname support | L7 visibility | When to use |
|---|---|---|---|---|
| NetworkPolicy egress | L3/L4 (IP+port) | No | No | Simple IP allowlists, no mesh |
| Istio Egress Gateway | L7 (hostname, path) | Yes | Yes | Full mesh already deployed |
| Cilium FQDN policy | L3/L4 + DNS name | Yes (DNS-aware) | Partial | Cilium CNI, no sidecar overhead |
| HTTP proxy (Squid etc) | L7 (URL/hostname) | Yes | Yes | Legacy 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
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 Token | SPIFFE SVID (SPIRE) | |
|---|---|---|
| Format | JWT (bound token) | X.509 certificate + optional JWT-SVID |
| TTL | 1 hour (projected), configurable | Configurable, typically 1 hour, auto-rotated |
| Scope | K8s API only | Any system — cross-cluster, multi-cloud |
| Mutual auth | Server only (no client cert) | Both sides prove identity (mTLS) |
| Cross-cluster | Not natively | Yes — federated trust bundles |
🧠 Knowledge Check
Q1. What does "mutual" mean in mutual TLS (mTLS)?
Q2. An Istio PeerAuthentication has mtls.mode: PERMISSIVE. What does this mean?
Q3. A SPIFFE SVID has the URI: spiffe://prod.acme.com/ns/billing/sa/invoice-svc. What does each part tell you?
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?
toFQDNs — DNS-aware egress without a full meshconnect syscall