Security

Service Mesh Security: mTLS with Istio and Linkerd

How to implement automatic mTLS between microservices using Istio and Linkerd, configure AuthorizationPolicy, handle cert rotation, and choose between them.

March 9, 20266 min readShipSafer Team

Service Mesh Security: mTLS with Istio and Linkerd

In a microservices architecture, traffic between services is often treated as implicitly trusted. If an attacker gains a foothold inside your cluster — through a compromised pod, a supply chain attack, or a container escape — they can reach every other service over unencrypted, unauthenticated internal traffic. A service mesh addresses this by implementing mutual TLS (mTLS) automatically between every service pair, without requiring changes to application code.

Why mTLS Between Services Matters

Standard TLS verifies the server's identity to the client. mTLS goes both directions: the server also verifies the client's identity. In a service mesh context, every workload gets a cryptographic identity (a SPIFFE SVID, typically a short-lived X.509 certificate), and all inter-service communication is encrypted and mutually authenticated.

This gives you:

  • Encryption in transit — no plaintext traffic between pods, even on the same node
  • Authentication — every service knows exactly which other service it is talking to
  • Authorization — you can write policy like "only the payments service may call orders on port 8080"
  • Audit trail — the mesh generates telemetry for every service-to-service call

Istio: Automatic mTLS with PeerAuthentication

Istio injects an Envoy sidecar into every pod. The sidecar intercepts all inbound and outbound traffic, handling TLS termination and mTLS negotiation transparently to the application.

Enabling Strict mTLS

By default, Istio runs in permissive mode — it accepts both mTLS and plaintext traffic. To enforce mTLS, apply a PeerAuthentication policy:

# Enforce mTLS for the entire mesh
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT

This single resource applied in the istio-system namespace enforces mTLS for every service in every namespace. Any plaintext traffic is rejected. You can also apply PeerAuthentication at the namespace or workload level for incremental rollout:

# Enforce mTLS only for the payments namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: payments-mtls
  namespace: payments
spec:
  mtls:
    mode: STRICT

AuthorizationPolicy: Service-Level Access Control

Once all traffic is mTLS, you can write authorization policies based on the verified identity of the calling service. These are enforced by the Envoy sidecar, not application code:

# Only allow the frontend service to call the orders service
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: orders-access
  namespace: default
spec:
  selector:
    matchLabels:
      app: orders
  action: ALLOW
  rules:
    - from:
        - source:
            principals:
              - "cluster.local/ns/default/sa/frontend"
    - to:
        - operation:
            methods: ["GET", "POST"]
            ports: ["8080"]

Without an explicit ALLOW policy, Istio denies all traffic when any AuthorizationPolicy exists for a workload. Add a default-deny policy to your namespaces to enforce this:

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: deny-all
  namespace: default
spec:
  {}

With deny-all in place, only explicitly permitted service-to-service calls will succeed.

Certificate Rotation in Istio

Istio's control plane (istiod) acts as a certificate authority. It issues short-lived certificates to workloads (default 24 hours) and rotates them automatically. This means even if a certificate is compromised, its useful lifetime is bounded.

# Check the certificate Istio issued to a pod
istioctl proxy-config secret POD_NAME -n NAMESPACE

# Verify rotation is working
kubectl exec -n istio-system \
  $(kubectl get pod -n istio-system -l app=istiod -o jsonpath='{.items[0].metadata.name}') \
  -- pilot-agent request GET /metrics | grep pilot_k8s_cfg_event_total

For production environments, integrate Istio with an external CA (cert-manager, Vault, or AWS ACM Private CA) rather than using istiod as a root CA. This allows your existing PKI infrastructure to be the root of trust:

# values.yaml for Istio Helm installation
pilot:
  env:
    EXTERNAL_CA: "true"
    K8S_SIGNER: "your-signer-name"

Linkerd: Simpler mTLS with Lower Overhead

Linkerd takes a different philosophy from Istio: minimal configuration, automatic mTLS by default, and a smaller resource footprint. It uses Rust-based micro-proxies instead of Envoy, which significantly reduces per-pod memory usage.

Installing Linkerd

# Install the CLI
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install | sh

# Validate your cluster
linkerd check --pre

# Install Linkerd
linkerd install --crds | kubectl apply -f -
linkerd install | kubectl apply -f -

# Verify installation
linkerd check

Injecting Linkerd into Workloads

Linkerd uses annotations to inject its proxy. You can inject at the namespace level (affects all pods in the namespace) or per-deployment:

# Annotate a namespace for automatic injection
apiVersion: v1
kind: Namespace
metadata:
  name: production
  annotations:
    linkerd.io/inject: enabled

Once injected, all TCP traffic between Linkerd-proxied workloads is automatically mTLS encrypted and authenticated. No additional configuration needed.

Verifying mTLS in Linkerd

# Check mTLS status for a deployment
linkerd viz stat deploy/frontend --namespace production

# See live traffic with mTLS indicators
linkerd viz tap deploy/frontend --namespace production

# Check edges (service-to-service connections)
linkerd viz edges pod --namespace production

The linkerd viz edges command shows all active connections and whether each is secured with mTLS. An unlocked padlock icon in the output indicates a non-mTLS connection — investigate any such connections.

Istio vs Linkerd: Choosing the Right Mesh

FactorIstioLinkerd
ProxyEnvoy (C++)Linkerd2-proxy (Rust)
Memory per pod~50-100MB~10-20MB
Configuration complexityHighLow
gRPC supportExcellentGood
Traffic management featuresExtensiveLimited
Default mTLSOpt-inAlways-on
Learning curveSteepGentle

For teams that primarily need mTLS and basic observability with minimal operational burden, Linkerd is the better choice. For teams that need advanced traffic management (circuit breaking, fault injection, sophisticated routing), Istio's additional capabilities justify the complexity.

Common Mistakes to Avoid

Not enabling STRICT mode in Istio. Permissive mode does not protect you — it only tells you that mTLS is available. Attackers inside the cluster can still send plaintext traffic.

Forgetting init containers and jobs. Sidecar injection may not apply to short-lived pods. Verify that batch jobs and init containers are also covered.

Trusting service identity without AuthorizationPolicy. mTLS gives you authentication but not authorization. Without AuthorizationPolicy, any service in the mesh can call any other. Defense-in-depth requires explicit allow policies.

Ignoring certificate rotation logs. Failed certificate rotations leave workloads with expired certs, causing connection failures. Monitor istiod/Linkerd control plane metrics for rotation errors.

A service mesh is one of the highest-leverage security controls you can implement in a microservices architecture. The network segment you thought was trusted becomes explicitly verified — and that assumption is one attackers frequently exploit.

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.