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.