How does Service A know that Service B is actually Service B?
In traditional networks, we trusted network location. If traffic came from the right IP, it was legitimate. Zero trust killed that assumption. Now every service must prove its identity, every time, regardless of network position.
SPIFFE (Secure Production Identity Framework for Everyone) is a standard for service identity. SPIRE is its production-ready implementation. Together, they give every workload a cryptographic identity — automatically, without static secrets.
The Problem: Service Identity is Hard
Traditional approaches to service authentication:
Shared secrets
# Every pod knows the same password
env:
- name: API_KEY
valueFrom:
secretKeyRef:
name: shared-secret
key: api-key
Problems:
- One compromised service exposes all services
- Rotation requires coordinated updates
- No identity distinction between services
Service accounts with tokens
# Kubernetes ServiceAccount tokens
automountServiceAccountToken: true
Better, but:
- Tokens are long-lived
- Work only within Kubernetes
- Limited attestation capabilities
mTLS with static certificates
# Manual certificate management
volumeMounts:
- name: certs
mountPath: /certs
Secure, but:
- Certificate management is operational overhead
- Rotation requires restarts
- Cross-cluster identity is complex
SPIFFE: The Standard
SPIFFE defines three things:
1. SPIFFE ID
A URI that uniquely identifies a workload:
spiffe://trust-domain/path/to/workload
Examples:
spiffe://example.com/ns/production/sa/api-server
spiffe://example.com/k8s/cluster-a/ns/default/pod/frontend-abc123
spiffe://example.com/aws/account-123/region/eu-west-1/instance/i-abc123
The format is consistent across all environments — Kubernetes, VMs, cloud instances, bare metal.
2. SVID (SPIFFE Verifiable Identity Document)
A short-lived credential proving the workload’s identity. Can be:
X.509 SVID — A certificate with the SPIFFE ID in the SAN
Subject Alternative Name:
URI: spiffe://example.com/ns/production/sa/api-server
JWT SVID — A signed JWT token
{
"sub": "spiffe://example.com/ns/production/sa/api-server",
"aud": ["spiffe://example.com/ns/production/sa/database"],
"exp": 1690000000
}
3. Workload API
A local API (usually a Unix socket) where workloads fetch their SVIDs:
/run/spire/sockets/agent.sock
Workloads never handle private keys directly. They request SVIDs from the Workload API, which handles key generation and rotation.
SPIRE: The Implementation
SPIRE has two components:
SPIRE Server
- Central authority that issues SVIDs
- Stores registration entries (which workloads get which identities)
- Manages trust bundles
SPIRE Agent
- Runs on every node
- Attests workloads (proves they are who they claim)
- Exposes the Workload API
- Caches and rotates SVIDs
flowchart TD
subgraph server["SPIRE Server"]
REG["Registration Entries"]
TRUST["Trust Bundles"]
end
server --> A1["Agent<br/>(Node1)"]
server --> A2["Agent<br/>(Node2)"]
server --> A3["Agent<br/>(Node3)"]
A1 --> W1["Workload API"]
A2 --> W2["Workload API"]
A3 --> W3["Workload API"]
Installing SPIRE on Kubernetes
Using Helm:
helm repo add spiffe https://spiffe.github.io/helm-charts-hardened
helm repo update
# Install SPIRE CRDs
helm install spire-crds spiffe/spire-crds \
--namespace spire-system \
--create-namespace
# Install SPIRE
helm install spire spiffe/spire \
--namespace spire-system \
--set global.spire.trustDomain=example.com
For GitOps with ArgoCD:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: spire-crds
namespace: argocd
spec:
project: default
source:
repoURL: https://spiffe.github.io/helm-charts-hardened
chart: spire-crds
targetRevision: 0.4.0
destination:
server: https://kubernetes.default.svc
namespace: spire-system
syncPolicy:
automated:
prune: true
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: spire
namespace: argocd
spec:
project: default
source:
repoURL: https://spiffe.github.io/helm-charts-hardened
chart: spire
targetRevision: 0.21.0
helm:
values: |
global:
spire:
trustDomain: example.com
clusterName: production
spire-server:
replicaCount: 3
dataStore:
sql:
databaseType: postgres
spire-agent:
socketPath: /run/spire/sockets/agent.sock
destination:
server: https://kubernetes.default.svc
namespace: spire-system
syncPolicy:
automated:
prune: true
Workload Registration
Workloads must be registered to receive identities. With SPIRE’s Kubernetes workload registrar, this happens automatically:
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterSPIFFEID
metadata:
name: api-server
spec:
spiffeIDTemplate: "spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}"
podSelector:
matchLabels:
app: api-server
namespaceSelector:
matchLabels:
spire-enabled: "true"
Any pod matching the selector in a labeled namespace gets the specified SPIFFE ID.
Using SVIDs in Applications
Option 1: SPIFFE Helper Sidecar
The SPIFFE helper sidecar fetches SVIDs and writes them to disk:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
template:
spec:
containers:
- name: app
image: my-app:v1.0.0
volumeMounts:
- name: spiffe
mountPath: /spiffe
readOnly: true
env:
- name: TLS_CERT
value: /spiffe/svid.pem
- name: TLS_KEY
value: /spiffe/svid_key.pem
- name: CA_BUNDLE
value: /spiffe/bundle.pem
- name: spiffe-helper
image: ghcr.io/spiffe/spiffe-helper:latest
volumeMounts:
- name: spiffe
mountPath: /spiffe
- name: spire-agent-socket
mountPath: /run/spire/sockets
volumes:
- name: spiffe
emptyDir: {}
- name: spire-agent-socket
hostPath:
path: /run/spire/sockets
type: Directory
Option 2: Native SPIFFE Library
For applications that integrate directly:
import (
"github.com/spiffe/go-spiffe/v2/workloadapi"
)
func main() {
ctx := context.Background()
// Connect to Workload API
source, err := workloadapi.NewX509Source(ctx)
if err != nil {
log.Fatal(err)
}
defer source.Close()
// Get SVID
svid, err := source.GetX509SVID()
if err != nil {
log.Fatal(err)
}
fmt.Printf("SPIFFE ID: %s\n", svid.ID)
// Use for mTLS
tlsConfig := tlsconfig.MTLSClientConfig(source, source, tlsconfig.AuthorizeID(
spiffeid.RequireID(spiffeid.MustParseSpiffeID("spiffe://example.com/ns/production/sa/database")),
))
}
Option 3: Envoy Sidecar with SDS
Envoy can fetch SVIDs via the Secret Discovery Service:
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
image: my-app:v1.0.0
ports:
- containerPort: 8080
- name: envoy
image: envoyproxy/envoy:v1.28-latest
volumeMounts:
- name: envoy-config
mountPath: /etc/envoy
- name: spire-agent-socket
mountPath: /run/spire/sockets
Envoy config for SDS:
static_resources:
clusters:
- name: spire_agent
connect_timeout: 1s
type: STATIC
http2_protocol_options: {}
load_assignment:
cluster_name: spire_agent
endpoints:
- lb_endpoints:
- endpoint:
address:
pipe:
path: /run/spire/sockets/agent.sock
- name: backend
connect_timeout: 1s
type: STRICT_DNS
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
common_tls_context:
tls_certificate_sds_secret_configs:
- name: "spiffe://example.com/ns/default/sa/api-server"
sds_config:
api_config_source:
api_type: GRPC
grpc_services:
- envoy_grpc:
cluster_name: spire_agent
validation_context_sds_secret_config:
name: "spiffe://example.com"
sds_config:
api_config_source:
api_type: GRPC
grpc_services:
- envoy_grpc:
cluster_name: spire_agent
Service Mesh Integration
SPIRE integrates with service meshes:
Istio
Istio can use SPIRE as its identity provider:
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
meshConfig:
trustDomain: example.com
values:
global:
caAddress: spiffe-csi-driver:443
Linkerd
Linkerd’s identity system is SPIFFE-compatible. Integration is straightforward with the identity controller.
Cross-Cluster Federation
SPIRE enables identity across cluster boundaries through federation:
Cluster A (production)
global:
spire:
trustDomain: production.example.com
federation:
enabled: true
bundleEndpoint:
address: spire-bundle.production.example.com
port: 8443
trustDomainBundles:
- endpointURL: https://spire-bundle.staging.example.com:8443
trustDomain: staging.example.com
Cluster B (staging)
global:
spire:
trustDomain: staging.example.com
federation:
enabled: true
bundleEndpoint:
address: spire-bundle.staging.example.com
port: 8443
trustDomainBundles:
- endpointURL: https://spire-bundle.production.example.com:8443
trustDomain: production.example.com
Now services in staging can verify identities from production and vice versa.
Attestation Methods
SPIRE proves workload identity through attestation. Kubernetes uses:
Node Attestation
# How nodes prove their identity to the server
nodeAttestor:
k8sPsat:
enabled: true
cluster: production
The node proves it’s a legitimate Kubernetes node using projected service account tokens.
Workload Attestation
# How workloads prove their identity to the agent
workloadAttestors:
k8s:
enabled: true
The agent verifies the workload’s container using the Kubernetes API.
Authorization with OPA
SPIRE provides identity; you still need authorization. Combine with Kyverno or OPA:
# OPA policy using SPIFFE IDs
package authz
default allow = false
allow {
# Allow api-server to access database
input.source == "spiffe://example.com/ns/production/sa/api-server"
input.destination == "spiffe://example.com/ns/production/sa/database"
}
allow {
# Allow frontend to access api-server
input.source == "spiffe://example.com/ns/production/sa/frontend"
input.destination == "spiffe://example.com/ns/production/sa/api-server"
}
My Production Setup
Here’s my SPIRE configuration:
global:
spire:
trustDomain: infrastructure.internal
clusterName: production
spire-server:
replicaCount: 3
dataStore:
sql:
databaseType: postgres
connectionString: "postgres://spire:password@postgres:5432/spire?sslmode=require"
nodeAttestor:
k8sPsat:
enabled: true
serviceAccountAllowList:
- spire-system:spire-agent
ca:
keyType: ec-p256
ttl: 24h
spire-agent:
socketPath: /run/spire/sockets/agent.sock
workloadAttestors:
k8s:
enabled: true
sds:
enabled: true
defaultBundleName: "null"
defaultAllBundlesName: ROOTCA
spiffe-csi-driver:
enabled: true
Key decisions:
- PostgreSQL backend — Persistent server state for HA
- EC-P256 keys — Balance of security and performance
- 24h TTL — SVIDs rotate frequently
- CSI driver — Clean volume-based SVID delivery
Troubleshooting
Check agent health
kubectl exec -n spire-system -it spire-agent-xxx -- \
/opt/spire/bin/spire-agent healthcheck
List registered workloads
kubectl exec -n spire-system -it spire-server-0 -- \
/opt/spire/bin/spire-server entry show
Verify workload identity
kubectl exec -it my-pod -- \
cat /run/spire/sockets/agent.sock # Check socket exists
kubectl exec -it my-pod -- \
openssl x509 -in /spiffe/svid.pem -text -noout | grep URI
Why This Matters
Static secrets are a liability. They can leak, they’re hard to rotate, and they don’t answer “who is making this request?”
SPIFFE/SPIRE provides:
- Automatic identity — Workloads get identity without manual secrets
- Short-lived credentials — SVIDs rotate frequently, limiting exposure
- Cryptographic proof — Can’t be forged or replayed
- Cross-environment — Same identity model everywhere
This is zero trust implemented correctly. Every service proves its identity with cryptographic evidence, every time, regardless of network position.
Understanding how workload identity works isn’t academic — it’s essential for building systems where compromising one service doesn’t compromise everything.
Identity should be automatic and cryptographically verifiable. SPIFFE and SPIRE give every workload an identity it doesn’t have to manage — and that attackers can’t steal.
