Deep Dive: Service Mesh Identity (SPIFFE/SPIRE)
What is SPIFFE/SPIRE?
SPIFFE (Secure Production Identity Framework for Everyone) is an open standard that defines how workloads — processes, containers, pods, functions — prove their identity in dynamic infrastructure. SPIRE (the SPIFFE Runtime Environment) is the reference implementation that automates identity issuance and rotation.
The problem SPIFFE solves is fundamental: in a modern Kubernetes cluster, pods are ephemeral. IP addresses change constantly, hostnames aren’t stable identifiers, and a workload’s identity shouldn’t depend on which node it’s running on. Traditional approaches (static API keys, per-host certificates) break down at this scale. SPIFFE introduces a new concept: workload identity — a cryptographically verifiable identity that follows the workload wherever it runs, tied to what it is (its code, namespace, service account) rather than where it is.
Core Concepts
- SPIFFE ID: A URI that uniquely identifies a workload:
spiffe://trust-domain/path. Example:spiffe://mycompany.com/production/payment-service. - SVID (SPIFFE Verifiable Identity Document): The credential that carries the SPIFFE ID. Can be an X.509 certificate (X509-SVID) or a JWT (JWT-SVID). X.509 SVIDs are most common for service mesh use.
- Trust Domain: The top-level namespace for SPIFFE IDs (
mycompany.comin the example). Corresponds to a SPIRE server or CA. - Attestation: The process by which SPIRE verifies a workload’s identity claims. Kubernetes attestation uses pod metadata (namespace, service account) to bind a SPIFFE ID to a workload.
- Workload API: A local Unix socket API that workloads (or their sidecars) use to fetch SVIDs without any network calls to the SPIRE server.
Authentication Flow
sequenceDiagram
participant W as Workload (Pod)
participant SA as SPIRE Agent (DaemonSet)
participant SS as SPIRE Server
participant CA as CA / Vault PKI
Note over W,SA: Kubernetes node startup
SA->>SS: Node attestation (k8s service account token)
SS->>SS: Verify node identity
SS-->>SA: Node SVID issued
Note over W,SA: Workload starts
W->>SA: Workload API: FetchX509SVID (via Unix socket)
SA->>SA: Workload attestation (pod UID, service account, namespace)
SA->>SS: Request SVID for workload
SS->>CA: Sign certificate with SPIFFE SAN
CA-->>SS: Signed X509 certificate
SS-->>SA: X509-SVID (cert + key + bundle)
SA-->>W: SVID delivered
Note over W: Certificate valid for 1 hour
Note over W: Auto-rotated before expiry
W->>W: mTLS with SVID (as client cert)
W->>W: Verify peer's SPIFFE ID via trust bundle
Part 1: SPIFFE ID Design
SPIFFE IDs are URIs with a hierarchical path. The path structure is flexible but should encode meaningful identity attributes:
spiffe://{trust-domain}/{path}
# Kubernetes: based on namespace + service account
spiffe://mycompany.com/k8s/production/ns/payment/sa/payment-service
# Alternative: by service name + environment
spiffe://mycompany.com/env/production/svc/payment-service
# Multi-cluster: include cluster name
spiffe://mycompany.com/cluster/us-west-2/ns/payment/sa/payment-service
Best practices for SPIFFE ID design:
- Include the environment (
production,staging,dev) - Include the Kubernetes namespace
- Include the service account name (not the pod name — pods are ephemeral)
- Don’t include the pod IP or node name — those change
- Keep the path human-readable for policy authoring
Part 2: SPIRE Architecture
SPIRE has two components: the SPIRE Server (cluster-scoped) and SPIRE Agent (node-scoped, runs as a DaemonSet):
SPIRE Server (1 per trust domain, HA replicas)
├── CA Plugin (issues SVIDs — can use upstream Vault PKI)
├── DataStore Plugin (stores registration entries — PostgreSQL in production)
├── Node Attestor Plugin (verifies node identity — k8s_psat)
└── Workload Attestor Plugin (maps pods to SPIFFE IDs — k8s)
SPIRE Agent (1 per node, DaemonSet)
├── Node Attestation (proves node identity to SPIRE Server)
├── Workload Attestation (verifies pod identity locally)
├── SVID Cache (holds current SVIDs, auto-rotates)
└── Workload API (Unix socket at /run/spire/sockets/agent.sock)
Deploy SPIRE in Kubernetes
# Install SPIRE using official Helm chart
helm repo add spiffe https://spiffe.github.io/helm-charts
helm repo update
# Install SPIRE with Kubernetes attestation
helm install spire spiffe/spire \
--namespace spire-system \
--create-namespace \
--set "global.spire.trustDomain=mycompany.com" \
--set "global.spire.clusterName=production" \
--set "spire-server.replicaCount=3" \
--set "spire-server.dataStore.database.databaseType=postgres" \
--set "spire-server.dataStore.database.connectionString=postgres://spire:password@postgres/spire"
SPIRE Server Configuration
# spire-server.conf
server {
bind_address = "0.0.0.0"
bind_port = "8081"
socket_path = "/run/spire/sockets/api.sock"
trust_domain = "mycompany.com"
data_dir = "/run/spire/data"
log_level = "INFO"
# JWT SVID expiry
jwt_issuer = "spire-server.spire-system.svc.cluster.local"
# CA TTL and SVID TTL
ca_ttl = "168h" # 7 days for CA certs
default_x509_svid_ttl = "1h" # 1 hour for service SVIDs
default_jwt_svid_ttl = "5m" # 5 minutes for JWT SVIDs
}
plugins {
# DataStore — use PostgreSQL in production
DataStore "sql" {
plugin_data {
database_type = "postgres"
connection_string = "postgres://spire:${POSTGRES_PASSWORD}@postgres.spire-system:5432/spire"
}
}
# Node attestor — Kubernetes projected service account tokens
NodeAttestor "k8s_psat" {
plugin_data {
clusters = {
"production" = {
service_account_allow_list = ["spire-system:spire-agent"]
kube_config_file = ""
allowed_pod_label_keys = []
allowed_node_label_keys = []
}
}
}
}
# Key manager — in-memory for dev, disk or AWS KMS for production
KeyManager "memory" {
plugin_data {}
}
# Upstream authority — optional: delegate to HashiCorp Vault PKI
# UpstreamAuthority "vault" {
# plugin_data {
# vault_addr = "https://vault.vault-system:8200"
# pki_mount_point = "spire-pki"
# cert_auth {
# cert_auth_mount_point = "cert"
# client_cert_path = "/run/spire/vault-cert/tls.crt"
# client_key_path = "/run/spire/vault-cert/tls.key"
# }
# }
# }
}
health_checks {
listener_enabled = true
bind_address = "0.0.0.0"
bind_port = "8080"
live_path = "/live"
ready_path = "/ready"
}
SPIRE Agent Configuration
# spire-agent.conf
agent {
data_dir = "/run/spire/data"
log_level = "INFO"
trust_domain = "mycompany.com"
server_address = "spire-server.spire-system.svc.cluster.local"
server_port = "8081"
socket_path = "/run/spire/sockets/agent.sock"
# How often to check for SVID expiry (rotate before expiry)
svc_account_key_file = ""
}
plugins {
# Node attestor
NodeAttestor "k8s_psat" {
plugin_data {
cluster = "production"
token_path = "/var/run/secrets/tokens/spire-agent"
}
}
# Workload attestor — Kubernetes pod metadata
WorkloadAttestor "k8s" {
plugin_data {
skip_kubelet_verification = false
}
}
# KeyManager — stores ephemeral keys in memory
KeyManager "memory" {
plugin_data {}
}
}
Part 3: Registration Entries
SPIRE needs to know which pods map to which SPIFFE IDs. Registration entries define this mapping:
# Register the payment-service workload
kubectl exec -n spire-system spire-server-0 -- \
/opt/spire/bin/spire-server entry create \
-spiffeID spiffe://mycompany.com/production/payment-service \
-parentID spiffe://mycompany.com/k8s/nodes \
-selector k8s:ns:production \
-selector k8s:sa:payment-service \
-selector k8s:container-name:payment-service \
-ttl 3600
# Register the order-service workload
kubectl exec -n spire-system spire-server-0 -- \
/opt/spire/bin/spire-server entry create \
-spiffeID spiffe://mycompany.com/production/order-service \
-parentID spiffe://mycompany.com/k8s/nodes \
-selector k8s:ns:production \
-selector k8s:sa:order-service \
-ttl 3600
# List all entries
kubectl exec -n spire-system spire-server-0 -- \
/opt/spire/bin/spire-server entry show
You can also manage entries via the SPIRE Kubernetes Controller Manager (SPIFFE/SPIRE project) using custom CRDs, which auto-creates entries from pod annotations:
# spiffeID annotation on Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
namespace: production
spec:
template:
metadata:
annotations:
spire.spiffe.io/spiffeID: "spiffe://mycompany.com/production/payment-service"
Part 4: Fetching SVIDs in Application Code
The SPIRE Workload API is exposed via a Unix socket. The @spiffe/spiffe-workload-api npm library wraps it:
import { WorkloadApiClient, X509Source } from '@spiffe/spiffe-workload-api';
// ============================================================================
// SVID Source — auto-rotates in background
// ============================================================================
async function createSvidSource(): Promise<X509Source> {
const source = await X509Source.create({
socketPath: process.env.SPIFFE_ENDPOINT_SOCKET ?? '/run/spire/sockets/agent.sock',
});
// Watch for SVID updates (rotation)
source.on('update', (svid) => {
console.info('SVID rotated', {
spiffeId: svid.spiffeId,
expiresAt: svid.certificates[0].notAfter,
});
});
return source;
}
// ============================================================================
// mTLS Server with SPIFFE SVIDs
// ============================================================================
import https from 'https';
import tls from 'tls';
async function createSpiffeMtlsServer(app: any): Promise<https.Server> {
const svidSource = await createSvidSource();
const svid = svidSource.svid;
const serverOptions: https.ServerOptions = {
cert: svid.certificates[0].raw,
key: svid.privateKey,
ca: svid.bundle,
requestCert: true,
rejectUnauthorized: true,
minVersion: 'TLSv1.3',
// Dynamic SNI — picks up new SVID on rotation without restart
SNICallback: (serverName, callback) => {
const current = svidSource.svid;
const ctx = tls.createSecureContext({
cert: current.certificates[0].raw,
key: current.privateKey,
ca: current.bundle,
});
callback(null, ctx);
},
};
return https.createServer(serverOptions, app);
}
// ============================================================================
// Extract and Validate SPIFFE ID from Peer Certificate
// ============================================================================
function getSpiffeIdFromRequest(req: any): string | null {
const socket = req.socket as tls.TLSSocket;
const cert = socket.getPeerCertificate(true);
if (!cert) return null;
// SPIFFE IDs are in the SAN URI field
const altNames: string = (cert as any).subjectaltname ?? '';
const match = altNames.match(/URI:(spiffe:\/\/[^,\s]+)/i);
return match ? match[1] : null;
}
// Trust is enforced at the TLS layer — peer cert must be signed by the trust bundle
// Application-level SPIFFE ID validation adds extra authorization checks
const ALLOWED_CALLERS: Record<string, string[]> = {
'/api/payments/charge': [
'spiffe://mycompany.com/production/order-service',
'spiffe://mycompany.com/production/subscription-service',
],
'/api/payments/refund': [
'spiffe://mycompany.com/production/order-service',
],
};
function spiffeAuthMiddleware(req: any, res: any, next: any): void {
const spiffeId = getSpiffeIdFromRequest(req);
if (!spiffeId) {
res.status(401).json({ error: 'No SPIFFE identity in client certificate' });
return;
}
const allowedCallers = ALLOWED_CALLERS[req.path];
if (allowedCallers && !allowedCallers.includes(spiffeId)) {
res.status(403).json({
error: 'SPIFFE identity not authorized for this endpoint',
identity: spiffeId,
});
return;
}
req.spiffeId = spiffeId;
next();
}// Maven: io.spiffe:java-spiffe-core, org.springframework.boot:spring-boot-starter-web
import io.spiffe.workloadapi.WorkloadApiClient;
import io.spiffe.workloadapi.DefaultWorkloadApiClient;
import io.spiffe.svid.x509svid.X509Svid;
import io.spiffe.spiffeid.SpiffeId;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.net.ssl.*;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;
@SpringBootApplication
public class PaymentServiceApplication {
private static final Logger log = Logger.getLogger(PaymentServiceApplication.class.getName());
// =========================================================================
// SVID Source — auto-rotates via WorkloadApiClient listener
// =========================================================================
@Bean
public WorkloadApiClient workloadApiClient() {
String socketPath = System.getenv().getOrDefault(
"SPIFFE_ENDPOINT_SOCKET", "unix:/run/spire/sockets/agent.sock"
);
return DefaultWorkloadApiClient.newClient(socketPath);
}
@Bean
public AtomicReference<X509Svid> currentSvid(WorkloadApiClient client) {
AtomicReference<X509Svid> ref = new AtomicReference<>();
// Fetch initial SVID
ref.set(client.fetchX509Svid());
// Watch for rotations
client.watchX509Svid(new WorkloadApiClient.X509SvidWatcher() {
@Override
public void onX509ContextUpdate(io.spiffe.workloadapi.X509Context context) {
X509Svid svid = context.getDefaultSvid();
ref.set(svid);
log.info("SVID rotated: " + svid.getSpiffeId()
+ " expires=" + svid.getCertificates().get(0).getNotAfter());
}
@Override
public void onError(Throwable e) {
log.severe("SVID watch error: " + e.getMessage());
}
});
return ref;
}
// =========================================================================
// mTLS Server — Spring Boot with SSLContext built from SPIFFE SVID
// Configure via application.properties: server.ssl.enabled=true
// The SSLContext is refreshed on each new connection via SNI callback
// =========================================================================
@Bean
public SSLContext spiffeSSLContext(AtomicReference<X509Svid> svidRef) throws Exception {
X509Svid svid = svidRef.get();
KeyManagerFactory kmf = KeyManagerFactory.getInstance("NewSunX509");
kmf.init(svid.getKeyStore(), "".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(svid.getTrustStore());
SSLContext ctx = SSLContext.getInstance("TLSv1.3");
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return ctx;
}
public static void main(String[] args) {
SpringApplication.run(PaymentServiceApplication.class, args);
}
}
// =========================================================================
// Extract and Validate SPIFFE ID from Peer Certificate
// =========================================================================
@Component
public class SpiffeAuthFilter extends OncePerRequestFilter {
private static final Map<String, List<String>> ALLOWED_CALLERS = Map.of(
"/api/payments/charge", List.of(
"spiffe://mycompany.com/production/order-service",
"spiffe://mycompany.com/production/subscription-service"
),
"/api/payments/refund", List.of(
"spiffe://mycompany.com/production/order-service"
)
);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String spiffeId = extractSpiffeId(request);
if (spiffeId == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\":\"No SPIFFE identity in client certificate\"}");
return;
}
List<String> allowed = ALLOWED_CALLERS.get(request.getServletPath());
if (allowed != null && !allowed.contains(spiffeId)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(
"{\"error\":\"SPIFFE identity not authorized\",\"identity\":\"" + spiffeId + "\"}"
);
return;
}
request.setAttribute("spiffeId", spiffeId);
chain.doFilter(request, response);
}
private String extractSpiffeId(HttpServletRequest request) {
X509Certificate[] certs = (X509Certificate[])
request.getAttribute("jakarta.servlet.request.X509Certificate");
if (certs == null || certs.length == 0) return null;
// SPIFFE IDs live in the SAN URI extension (OID 2.5.29.17)
try {
Collection<List<?>> sans = certs[0].getSubjectAlternativeNames();
if (sans == null) return null;
for (List<?> san : sans) {
// SAN type 6 = URI
if (Integer.valueOf(6).equals(san.get(0))) {
String uri = (String) san.get(1);
if (uri.startsWith("spiffe://")) return uri;
}
}
} catch (Exception e) { /* ignore */ }
return null;
}
}# pip install spiffe fastapi uvicorn[standard] cryptography
import os
import asyncio
import logging
from typing import Optional
from collections import defaultdict
from spiffe import WorkloadApiClient, X509Source
from fastapi import FastAPI, Request, HTTPException, Depends
from cryptography.x509.oid import ExtensionOID
from cryptography.x509 import SubjectAlternativeName, UniformResourceIdentifier
import ssl
logger = logging.getLogger(__name__)
app = FastAPI()
# =============================================================================
# SVID Source — auto-rotates in background
# =============================================================================
_x509_source: Optional[X509Source] = None
async def get_x509_source() -> X509Source:
global _x509_source
if _x509_source is None:
socket_path = os.getenv(
"SPIFFE_ENDPOINT_SOCKET", "unix:///run/spire/sockets/agent.sock"
)
_x509_source = await X509Source.create(socket_path=socket_path)
logger.info("X509Source created, SVID: %s", _x509_source.svid.spiffe_id)
def on_update(svid):
logger.info(
"SVID rotated spiffe_id=%s expires=%s",
svid.spiffe_id,
svid.leaf.not_valid_after,
)
_x509_source.set_on_update(on_update)
return _x509_source
# =============================================================================
# mTLS Server — build SSLContext from SPIFFE SVID
# =============================================================================
async def create_spiffe_ssl_context() -> ssl.SSLContext:
source = await get_x509_source()
svid = source.svid
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
ctx.verify_mode = ssl.CERT_REQUIRED
# Write PEM data to ctx (in production, use tempfile or in-memory BIO)
ctx.load_verify_locations(cadata=svid.bundle_pem.decode())
ctx.load_cert_chain(certfile=None, keyfile=None) # use load_cert_chain with PEM bytes
return ctx
# =============================================================================
# Extract and Validate SPIFFE ID from Peer Certificate
# =============================================================================
ALLOWED_CALLERS: dict[str, list[str]] = {
"/api/payments/charge": [
"spiffe://mycompany.com/production/order-service",
"spiffe://mycompany.com/production/subscription-service",
],
"/api/payments/refund": [
"spiffe://mycompany.com/production/order-service",
],
}
def extract_spiffe_id(cert) -> Optional[str]:
"""Extract SPIFFE ID from X.509 SAN URI extension."""
try:
san_ext = cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
for uri in san_ext.value.get_values_for_type(UniformResourceIdentifier):
if uri.startswith("spiffe://"):
return uri
except Exception:
pass
return None
async def spiffe_auth(request: Request) -> str:
"""FastAPI dependency — verifies SPIFFE mTLS identity."""
# Peer cert is available on the transport in ASGI when using SSL
transport = request.scope.get("transport")
cert = getattr(transport, "get_extra_info", lambda _: None)("peercert_der")
if cert is None:
raise HTTPException(status_code=401, detail="No SPIFFE identity in client certificate")
from cryptography import x509
parsed = x509.load_der_x509_certificate(cert)
spiffe_id = extract_spiffe_id(parsed)
if not spiffe_id:
raise HTTPException(status_code=401, detail="No SPIFFE identity in client certificate")
allowed = ALLOWED_CALLERS.get(request.url.path)
if allowed is not None and spiffe_id not in allowed:
raise HTTPException(
status_code=403,
detail={"error": "SPIFFE identity not authorized", "identity": spiffe_id},
)
return spiffe_id
@app.post("/api/payments/charge")
async def charge_payment(spiffe_id: str = Depends(spiffe_auth)):
return {"status": "charged", "caller": spiffe_id}// dotnet add package Microsoft.AspNetCore.Authentication.Certificate
// dotnet add package Spiffe.AspNetCore (community SPIFFE library)
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
var builder = WebApplication.CreateBuilder(args);
// =============================================================================
// SVID Source — configure Kestrel with SPIFFE SVID (auto-rotating)
// =============================================================================
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(8443, listenOptions =>
{
listenOptions.UseHttps(httpsOptions =>
{
// In production: use a SpiffeX509Source that auto-rotates the cert
// Here we load from environment for illustration
var certPem = Environment.GetEnvironmentVariable("SERVICE_CERT_PEM")!;
var keyPem = Environment.GetEnvironmentVariable("SERVICE_KEY_PEM")!;
var cert = X509Certificate2.CreateFromPem(certPem, keyPem);
httpsOptions.ServerCertificate = cert;
httpsOptions.ClientCertificateMode =
Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.RequireCertificate;
});
});
});
// Certificate authentication
builder.Services
.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.RevocationMode = X509RevocationMode.NoCheck; // SPIFFE SVIDs use short TTLs
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var spiffeId = ExtractSpiffeId(context.ClientCertificate);
if (spiffeId == null)
{
context.Fail("No SPIFFE identity in client certificate");
return Task.CompletedTask;
}
// Store SPIFFE ID as a claim for use in authorization
var claims = new[] { new System.Security.Claims.Claim("spiffe_id", spiffeId) };
context.Principal = new System.Security.Claims.ClaimsPrincipal(
new System.Security.Claims.ClaimsIdentity(claims, context.Scheme.Name)
);
context.Success();
return Task.CompletedTask;
},
};
});
builder.Services.AddAuthorization();
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// =============================================================================
// Extract and Validate SPIFFE ID from Peer Certificate
// =============================================================================
static string? ExtractSpiffeId(X509Certificate2 cert)
{
// SPIFFE ID is in the SAN URI extension (OID 2.5.29.17)
foreach (var ext in cert.Extensions)
{
if (ext.Oid?.Value == "2.5.29.17")
{
var sanData = ext.Format(false);
foreach (var entry in sanData.Split(',', StringSplitOptions.TrimEntries))
{
if (entry.StartsWith("URL=spiffe://") || entry.StartsWith("URI=spiffe://"))
return entry[(entry.IndexOf("spiffe://"))..];
}
}
}
return null;
}
// =============================================================================
// SPIFFE Authorization middleware
// =============================================================================
static readonly Dictionary<string, string[]> AllowedCallers = new()
{
["/api/payments/charge"] = [
"spiffe://mycompany.com/production/order-service",
"spiffe://mycompany.com/production/subscription-service"
],
["/api/payments/refund"] = [
"spiffe://mycompany.com/production/order-service"
],
};
app.Use(async (context, next) =>
{
if (AllowedCallers.TryGetValue(context.Request.Path, out var allowed))
{
var spiffeId = context.User.FindFirst("spiffe_id")?.Value;
if (spiffeId == null || !allowed.Contains(spiffeId))
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsJsonAsync(new
{
error = "SPIFFE identity not authorized for this endpoint",
identity = spiffeId
});
return;
}
}
await next();
});
app.MapControllers();
app.Run();Part 5: Istio Integration
Istio is the most popular service mesh and uses SPIFFE SVIDs natively. With Istio, your application code doesn’t handle mTLS at all — the Envoy sidecar does everything:
Enable Strict mTLS Across the Mesh
# peer-authentication.yaml — Require mTLS for all services in production namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # Reject all non-mTLS traffic
Authorization Policy Using SPIFFE IDs
# authz-payment-service.yaml — Only allow order-service to call payment-service
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: payment-service-authz
namespace: production
spec:
selector:
matchLabels:
app: payment-service
rules:
- from:
- source:
principals:
# Istio SPIFFE ID format: cluster.local/ns/{namespace}/sa/{service-account}
- "cluster.local/ns/production/sa/order-service"
- "cluster.local/ns/production/sa/subscription-service"
to:
- operation:
methods: ["POST"]
paths: ["/api/payments/charge"]
- from:
- source:
principals:
- "cluster.local/ns/production/sa/order-service"
to:
- operation:
methods: ["POST"]
paths: ["/api/payments/refund"]
Verify mTLS is Working
# Check peer authentication status for a namespace
kubectl get peerauthentication -n production
# Check connection security between services
istioctl x describe pod payment-service-pod-xyz -n production
# Check SPIFFE IDs assigned to pods
kubectl exec -n production payment-service-pod-xyz -c istio-proxy -- \
curl -s http://localhost:15000/certs | jq '.certificates[].cert_chain[0].subject_alt_names'
# Verify mTLS between two services
istioctl authn tls-check order-service.production.svc.cluster.local
Linkerd Alternative
Linkerd is simpler to operate than Istio and also uses SPIFFE:
# Install Linkerd
linkerd install --crds | kubectl apply -f -
linkerd install | kubectl apply -f -
# Annotate namespace for automatic proxy injection
kubectl annotate namespace production linkerd.io/inject=enabled
# Check mTLS status
linkerd viz stat deployment -n production
linkerd edges po -n production
# Linkerd authorization policy
apiVersion: policy.linkerd.io/v1beta3
kind: AuthorizationPolicy
metadata:
name: payment-service-authz
namespace: production
spec:
targetRef:
group: core
kind: Service
name: payment-service
requiredAuthenticationRefs:
- name: order-service-identity
kind: MeshTLSAuthentication
group: policy.linkerd.io
---
apiVersion: policy.linkerd.io/v1beta3
kind: MeshTLSAuthentication
metadata:
name: order-service-identity
namespace: production
spec:
identityRefs:
- kind: ServiceAccount
name: order-service
namespace: production
Part 6: Zero-Trust Networking with SPIFFE
The full zero-trust vision with SPIFFE means no service is trusted by default — every request is authenticated and authorized:
# 1. Deny all traffic by default in the production namespace
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: deny-all
namespace: production
spec: {} # Empty spec = deny all
---
# 2. Allow specific service-to-service communication
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: allow-order-to-payment
namespace: production
spec:
selector:
matchLabels:
app: payment-service
rules:
- from:
- source:
principals:
- "cluster.local/ns/production/sa/order-service"
to:
- operation:
methods: ["POST"]
paths: ["/api/payments/charge", "/api/payments/refund"]
---
# 3. Allow health check from load balancer
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: allow-health-check
namespace: production
spec:
selector:
matchLabels:
app: payment-service
rules:
- to:
- operation:
methods: ["GET"]
paths: ["/health", "/ready"]
Part 7: Production Checklist
SPIRE Infrastructure
- SPIRE Server deployed with 3+ replicas (HA)
- PostgreSQL backend for DataStore (not SQLite)
- SPIRE Server connected to Vault PKI as upstream authority
- SPIRE Agent deployed as DaemonSet on all nodes
- Agent node attestation uses Projected Service Account Tokens (PSAT), not legacy SAT
- SVID TTL set to 1 hour or less
- CA TTL set to 7 days (shorter rotation = smaller blast radius)
Registration Entries
- Every service has a registration entry
- Entries use minimum selectors (namespace + service account is sufficient)
- Registration entry management is automated (SPIRE Controller Manager CRDs)
- Test that a pod with wrong service account cannot obtain the expected SVID
Istio/Service Mesh
- PeerAuthentication STRICT mode enabled for production namespace
- AuthorizationPolicy: deny-all baseline with explicit allow rules
- Verify mTLS status with
istioctl authn tls-check - Monitor Envoy sidecar certificate expiry metrics
- Test that inter-service calls fail without proper SVID
Operational
- Monitor SPIRE Agent connectivity to SPIRE Server
- Alert on SPIRE Agent down (workloads can’t get new SVIDs)
- Audit log for SPIFFE ID registrations and deregistrations
- Runbook for SPIRE Server failure and recovery
- Test SVID rotation does not interrupt in-flight requests
- Monitor trust bundle distribution lag
Conclusion
SPIFFE/SPIRE represents the state of the art in workload identity — cryptographic, automated, transparent to application code, and built for the dynamic reality of Kubernetes workloads. The identity is tied to what a service is (its namespace, service account, running pod) rather than static secrets that leak and expire.
The operational investment is significant. SPIRE is infrastructure that must be highly available — if the SPIRE Server cluster is down, workloads can’t get new SVIDs (though they keep serving with cached SVIDs until those expire). You need to own the full stack: SPIRE Server HA, PostgreSQL for the datastore, Vault PKI for the upstream CA, and either Istio or Linkerd for mesh-level enforcement.
For teams operating at scale in Kubernetes — dozens or hundreds of services, multi-cluster deployments, strict compliance requirements — the investment pays for itself in eliminated secret management toil and audit confidence. For smaller deployments, start with mTLS via cert-manager or OAuth 2.0 client credentials, and graduate to SPIFFE/SPIRE when the operational maturity is there to support it.