PRACTITIONER GUIDE | CLOUD SECURITY
Practitioner GuideUpdated 14 min read

Istio Service Mesh: Enforcing mTLS and SPIFFE Identity Between Microservices

STRICT
PeerAuthentication mode required for enforced mTLS -- PERMISSIVE mode accepts plaintext and provides no security guarantee
24h
Default SVID certificate rotation interval in Istio -- short-lived certs limit blast radius of a compromised workload identity
0
AuthorizationPolicy resources needed to allow all traffic -- the default is allow-all, requiring explicit deny or allow policies to restrict it
spiffe://
URI scheme for SPIFFE identities -- Istio encodes service account as spiffe://cluster.local/ns/<ns>/sa/<serviceaccount> in each SVID

SponsoredRetool

Retool's new app builder is where AI-generated code ships safely

Building apps with AI is easy. Getting them to production safely is another story.

Start building for free today

Istio's default configuration is deceptively insecure: sidecars are injected but PeerAuthentication is PERMISSIVE, meaning plaintext connections are accepted silently alongside mTLS. AuthorizationPolicy resources are absent, so every service can reach every other service on any port. The result is a service mesh that provides observability but no zero-trust enforcement. Moving from this default to a genuinely secure Istio deployment requires three layered changes: enforcing STRICT mTLS via PeerAuthentication, issuing SPIFFE SVIDs for every workload identity, and writing AuthorizationPolicy resources that restrict service communication to explicitly permitted paths. This guide covers each layer, the rollout sequence that avoids production breakage, and the JWT configuration that adds end-user authentication at the ingress layer.

Understand SPIFFE Identity and Citadel CA Before Configuring Policies

Istio's security model depends on cryptographic service identity. Before writing PeerAuthentication or AuthorizationPolicy resources, verify that Citadel is issuing SVIDs correctly and that sidecars are present in all target namespaces.

Verify sidecar injection is enabled per namespace

Check which namespaces have automatic sidecar injection: `kubectl get namespace -L istio-injection`. Namespaces without the `istio-injection=enabled` label will not have sidecars, and applying STRICT PeerAuthentication to those namespaces breaks all ingress traffic. Label target namespaces: `kubectl label namespace <ns> istio-injection=enabled`. Restart existing pods (rolling restart) after labeling to inject sidecars into already-running workloads: `kubectl rollout restart deployment -n <ns>`.

Confirm SVID issuance for a running pod

Verify the sidecar has a valid SPIFFE certificate: `istioctl proxy-config secret <pod-name>.<namespace>`. Look for a CERTIFICATE entry with a SPIFFE URI in the SAN field matching spiffe://cluster.local/ns/<namespace>/sa/<serviceaccount>. If the certificate is missing or expired, check istiod logs for certificate signing errors: `kubectl logs -n istio-system -l app=istiod | grep -i cert`. A healthy Citadel CA signs certificate requests within seconds of pod startup.

Review Citadel CA configuration in istiod

Istio 1.15+ uses the istiod component for CA functions (previously a separate Citadel pod). Configure SVID lifetime via the `CITADEL_WORKLOAD_CERT_TTL` environment variable on istiod. The default 24-hour TTL is appropriate for most environments. For high-security workloads, reduce to 1-4 hours. Shorter TTL means certificates rotate more frequently, reducing the window where a stolen certificate is usable, but increases load on istiod for large clusters.

Use istioctl to inspect mTLS status between services

Before enforcing STRICT mode, audit current mTLS status: `istioctl authn tls-check <source-pod> <destination-service>.<namespace>.svc.cluster.local`. The output shows the applied PeerAuthentication mode, the DestinationRule TLS mode, and whether the connection currently uses mTLS or plaintext. Run this check for every service pair in your target namespace to identify which pairs will fail under STRICT enforcement before making the change.

Enforce Strict mTLS with PeerAuthentication

PeerAuthentication resources control the mTLS mode at the mesh, namespace, or workload level. Roll out STRICT mode namespace by namespace to avoid cluster-wide disruptions.

Apply namespace-scoped STRICT PeerAuthentication

Create a PeerAuthentication resource in the target namespace: `apiVersion: security.istio.io/v1beta1, kind: PeerAuthentication, metadata: {name: default, namespace: <ns>}, spec: {mtls: {mode: STRICT}}`. The name `default` applies the policy to all workloads in the namespace. After applying, verify with `istioctl authn tls-check` that connections between services in the namespace now show mTLS active. Monitor Envoy access logs for PEER_CERT_REQUIRED errors indicating services that lack sidecars.

Use port-level exceptions for workloads with mixed traffic

Some workloads receive traffic from both sidecar-injected services and external clients without mTLS capability (health check probes from cloud load balancers, Prometheus scraping without a sidecar). Use port-level mTLS mode overrides: in the PeerAuthentication spec, add `portLevelMtls: {9090: {mode: PERMISSIVE}}` to allow plaintext on the metrics port while enforcing STRICT on all other ports. This is preferable to setting the entire workload to PERMISSIVE just to accommodate health checks.

Apply a mesh-wide STRICT policy after namespace rollout completes

Once all namespaces have STRICT PeerAuthentication and all pods have sidecars, apply a mesh-wide default in the istio-system namespace: create a PeerAuthentication with namespace `istio-system` and no selector. This becomes the baseline for any new namespace created in the cluster. Namespace-level policies override the mesh-wide policy, so existing namespace-specific configurations remain effective. Test by deploying a new namespace without explicit PeerAuthentication -- it should inherit STRICT mode.

Free daily briefing

Briefings like this, every morning before 9am.

Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.

Write AuthorizationPolicy Resources to Restrict Lateral Movement

Without AuthorizationPolicy, STRICT mTLS authenticates services but does not restrict which authenticated services can call each other. AuthorizationPolicy adds the authorization layer.

Start with a default-deny policy per namespace

Apply a default-deny AuthorizationPolicy that blocks all traffic not matched by an explicit ALLOW rule: `apiVersion: security.istio.io/v1beta1, kind: AuthorizationPolicy, metadata: {name: deny-all, namespace: <ns>}, spec: {}`. An empty spec means no rules, which Istio interprets as deny-all once any AuthorizationPolicy exists in the namespace. Apply this only after writing explicit ALLOW rules for all legitimate service communications -- applying deny-all first breaks everything.

Write ALLOW policies based on SPIFFE source principal

Allow traffic from specific services using the source.principal field which matches the SPIFFE URI: `spec: {rules: [{from: [{source: {principals: ['cluster.local/ns/payments/sa/checkout-service']}}], to: [{operation: {methods: ['POST'], paths: ['/api/v1/charge']}}]}]}`. This policy on the payments service allows only the checkout-service (identified by its SPIFFE SVID) to call POST /api/v1/charge. All other callers, including other authenticated services, are denied. Avoid using source.namespaces alone -- it allows any service in that namespace, not just the intended caller.

Add RequestAuthentication for JWT end-user validation at ingress

Apply RequestAuthentication to the ingress gateway or to individual services that receive external traffic: `spec: {jwtRules: [{issuer: 'https://accounts.google.com', jwksUri: 'https://www.googleapis.com/oauth2/v3/certs'}]}`. Then add an AuthorizationPolicy requiring the JWT claim: `spec: {rules: [{from: [{source: {requestPrincipals: ['https://accounts.google.com/*']}}]}]}`. The requestPrincipals field matches the iss/sub claim combination, ensuring requests without a valid JWT from the configured issuer are denied.

Audit AuthorizationPolicy coverage across the mesh

Use `istioctl analyze` to detect namespaces with no AuthorizationPolicy (default allow-all). Also check for overly broad policies: any AuthorizationPolicy using `source.namespaces` without a specific `source.principals` list allows any service in that namespace to call the destination -- this is too permissive for services handling sensitive data. Review policies quarterly as service accounts are added and removed.

Monitor mTLS and Authorization Enforcement with Kiali and Prometheus

Ongoing visibility into mTLS health and policy violations is essential for detecting misconfigurations and service-to-service access anomalies.

Use Kiali to visualize mTLS status across the service graph

Kiali (the Istio observability UI) shows mTLS lock icons on service graph edges indicating whether each connection uses mTLS or plaintext. A red or open lock indicates a connection that bypasses mTLS enforcement. Access Kiali: `istioctl dashboard kiali`. Navigate to the Graph view and filter by namespace. Any plaintext connection in a namespace with STRICT PeerAuthentication indicates a service without a sidecar or a misconfigured DestinationRule overriding mTLS.

Alert on AuthorizationPolicy denials via Prometheus

Istio exposes the metric `istio_requests_total` with label `response_code=403` for AuthorizationPolicy denials. Create a Prometheus alert: `rate(istio_requests_total{response_code='403'}[5m]) > 0.1` scoped to production namespaces. A spike in 403 responses from a specific source service indicates either a misconfigured policy blocking legitimate traffic or an unauthorized service attempting cross-namespace access. Both warrant immediate investigation.

The bottom line

Istio's default PERMISSIVE mTLS and absent AuthorizationPolicy make it an observability tool, not a security control. The path to genuine zero-trust enforcement is sequential: inject sidecars everywhere, enforce STRICT PeerAuthentication namespace by namespace, then add default-deny AuthorizationPolicy with explicit ALLOW rules per service pair based on SPIFFE principal. The SPIFFE identity model is what makes this scalable: policies reference cryptographic service account identities that survive pod restarts and IP changes, not fragile IP-based ACLs.

Frequently asked questions

What is the difference between PeerAuthentication PERMISSIVE and STRICT mode?

PERMISSIVE mode accepts both mTLS and plaintext connections. It is the default and is intended as a migration aid: existing services without sidecars can still communicate while you gradually roll out Istio. PERMISSIVE mode provides no security guarantee because an attacker or misconfigured service can send plaintext and bypass mTLS entirely. STRICT mode rejects any connection that does not present a valid SPIFFE SVID certificate, enforcing mutual authentication. Always target STRICT mode for production namespaces once all services in that namespace have Istio sidecars injected.

How does Istio issue SPIFFE SVIDs to workloads?

Istio's istiod control plane includes Citadel, a certificate authority that issues X.509 certificates encoding SPIFFE URIs. When a pod with Istio injection starts, the Envoy sidecar connects to istiod via a secure gRPC channel using the node's kubelet credentials to prove the pod's Kubernetes service account identity. Citadel issues an X.509 SVID valid for 24 hours (configurable) with the SPIFFE URI spiffe://cluster.local/ns/<namespace>/sa/<serviceaccount> in the SAN field. The sidecar stores the certificate in memory and rotates it automatically before expiry, never touching the pod's filesystem.

How do AuthorizationPolicy resources restrict service-to-service traffic?

AuthorizationPolicy resources are applied to the destination service (where traffic arrives) and specify ALLOW or DENY rules based on source principal (SPIFFE identity), namespace, IP block, HTTP method, path, and JWT claims. The default behavior with no AuthorizationPolicy is allow-all. Once any ALLOW AuthorizationPolicy exists on a workload, Istio switches to default-deny for that workload and only permits traffic matching the ALLOW rules. This means you must write explicit ALLOW rules for all legitimate traffic before adding any AuthorizationPolicy to a namespace.

What is RequestAuthentication and how does it differ from PeerAuthentication?

PeerAuthentication handles service-to-service mTLS: it authenticates the calling workload using its SPIFFE SVID certificate. RequestAuthentication handles end-user authentication: it validates JWT tokens in the Authorization header of HTTP requests using a configured JWKS URI (your OIDC provider's public key endpoint). RequestAuthentication alone does not deny requests without valid tokens -- it only validates tokens when present. Pair it with an AuthorizationPolicy requiring the `request.auth.claims[iss]` field to restrict unauthenticated requests.

How do you roll out STRICT mTLS without breaking existing services?

Use a namespace-by-namespace migration: start with development namespaces, apply a PeerAuthentication in STRICT mode, verify no traffic breaks by checking Kiali or Envoy access logs for connection errors. Move to staging, then production namespaces one at a time. For namespaces with services that call external systems without Istio sidecars, use DestinationRule traffic policy to exclude those specific hosts from mTLS. Never apply a mesh-wide STRICT PeerAuthentication until every pod in every namespace has a sidecar -- use namespace-scoped PeerAuthentication resources during migration.

How do you debug mTLS failures in Istio?

Start with `istioctl proxy-config listeners <pod>` to verify the sidecar is configured and listening. Check `istioctl authn tls-check <pod> <destination-service>` to see the effective mTLS mode between two services. Review Envoy access logs for SSL_ERROR or PEER_CERT_REQUIRED entries. Use `istioctl analyze` to detect AuthorizationPolicy or PeerAuthentication misconfigurations. For JWT validation failures, check the Envoy filter configuration for the jwt_authn filter and verify the JWKS URI is reachable from within the cluster.

Can Istio enforce mTLS for egress traffic to external services?

Istio controls egress via EgressGateway and ServiceEntry resources. To enforce TLS origination for external services, create a ServiceEntry for the external host and a DestinationRule with `tls.mode: SIMPLE` (one-way TLS) or `tls.mode: MUTUAL` (mTLS with client certificates). Route all egress through an EgressGateway to centralize monitoring and policy enforcement. Without an EgressGateway, pods can bypass Istio egress policy by connecting directly to external IPs. Lock down direct external access with Kubernetes NetworkPolicy blocking egress except to the EgressGateway.

Sources & references

  1. Istio Security Concepts Documentation
  2. Istio PeerAuthentication Reference
  3. Istio Authorization Policy Reference
  4. SPIFFE Standard Documentation

Free resources

25
Free download

Critical CVE Reference Card 2025–2026

25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.

No spam. Unsubscribe anytime.

Free download

Ransomware Incident Response Playbook

Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.

No spam. Unsubscribe anytime.

Free newsletter

Get threat intel before your inbox does.

50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.

Unsubscribe anytime. We never sell your data.

Eric Bang
Author

Founder & Cybersecurity Evangelist, Decryption Digest

Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.

Black Hat Giveaway

Win a $2,495 Black Hat pass.

Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.

Joins Decryption Digest daily briefing. Unsubscribe anytime.

Giveaway: Black Hat USA 2026 Full-Access Pass ($2,495 value)

Details →
Daily Briefing

Subscribe to enter the giveaway

Every subscriber is automatically entered. You also get daily threat intel every morning: zero-days, ransomware, and nation-state campaigns. Free. No spam.

Already subscribed? You're already entered.

Giveaway

Win a $2,495 Black Hat USA 2026 pass.