Skip to content
Hogin Hogin
Go back

SPIFFE/SPIRE: cryptographic workload identity instead of static secrets

11 мин чтения

Every external-secrets-operator, every Vault Agent Injector, and every CI job that hands pods keys and tokens solves the same problem with the same weak spot: somewhere, a secret has to sit that anyone with access to its store — or to the pod itself — can read. SPIFFE flips the question — instead of “what does the pod know,” it asks “what can the pod cryptographically prove it is,” and it does that with no pre-shared value at all. SPIRE, the reference implementation of SPIFFE, has already become the standard identity layer under service meshes like Cilium and Istio. Let’s break down how it works and why it’s fundamentally different from handing out secrets.

Table of contents

Open Table of contents

Why this can’t wait until 2026

Zero-trust architecture is built on continuously verifying “who is this” on every network call, not on a perimeter. The problem is that most existing workload identity mechanisms — API keys, static certificates, long-TTL service account tokens — are exactly what zero trust is supposed to move away from: a value that can be copied, dropped into a config, and used from anywhere until someone notices the leak. SPIFFE (Secure Production Identity Framework For Everyone) standardizes a different approach — identity issued on the fly based on what the workload actually is at runtime (a pod in a namespace, running under a service account, on a specific node), not what someone once put into an environment variable.

A practical signal that this is already mainstream, not an experimental spec: Cilium — one of the most widely deployed CNIs and service meshes in Kubernetes — integrates with SPIFFE/SPIRE precisely as its workload identity layer for pod-to-pod mTLS (integration walkthrough). If you’re already building a zero-trust network on a service mesh, the question isn’t “do I need SPIFFE” — it’s “what’s it already running under the hood.”

SVID: how a pod proves who it is

The unit of identity in SPIFFE is the SVID (SPIFFE Verifiable Identity Document). It’s not a token the pod stores somewhere — it’s a document that the SPIRE Agent issues on request and continuously reissues. The SPIFFE ID inside an SVID looks like a URI: spiffe://example.org/ns/default/sa/checkout-api — a trust domain plus a path that uniquely identifies the workload.

X.509-SVID and JWT-SVID: different formats for different scenarios

SPIFFE defines two SVID formats, and picking between them isn’t a matter of taste:

Both are issued by the same SPIRE Agent through the same API — the format you pick depends on what’s on the other end, not on what infrastructure you happen to have deployed.

Node attestation and workload attestation: identity with no secret

The key question: if the pod isn’t storing a secret, how does SPIRE know it’s talking to a legitimate workload and not an impostor? The answer is two-tier attestation, and neither tier requires a pre-shared value.

Two-tier attestation: node attestation → workload attestation → SVID

Node attestation happens once, when the SPIRE Agent (typically a DaemonSet) starts on a node. The agent proves to the SPIRE Server exactly which node it’s running on — through a platform-level signal that can’t be forged without compromising the platform itself: the node’s projected service account token in Kubernetes (k8s_psat), an instance identity document on AWS, a TPM quote on bare metal. The server cross-checks this against the platform’s actual state (for example, asking the Kubernetes API whether that agent pod really exists in that namespace) and issues the node a Node SVID.

Workload attestation happens on every request a local process makes to the agent. The agent doesn’t ask the process for a password — it looks at the mere fact that the process exists, through the OS and the platform: the PID, the cgroup, and for Kubernetes, a call to kubelet, from which the agent learns the namespace, service account, and labels of the pod that PID belongs to. These attributes — selectors — are matched against entries registered on the server, and if a match is found, the agent issues an SVID with the corresponding SPIFFE ID.

At no step is there a value that had to be generated ahead of time, dropped into a secret store, and hoped not to leak. Identity is derived from the platform’s observable state at the moment of the request.

How this differs from external-secrets-operator

These aren’t competing tools — they solve different problems, and most zero-trust architectures need both.

external-secrets-operator syncs values from an external secret store (Vault, AWS Secrets Manager, GCP Secret Manager) into a Kubernetes Secret, so a pod can mount it or read it as an environment variable. This is secret distribution: credential material gets materialized, sits in etcd (encrypted at rest or not), and has to be rotated on a schedule — manually or via a refreshInterval.

SPIRE distributes nothing. An SVID is issued on request through the Workload API and is never persisted past its own TTL. This is identity proof: the pod doesn’t store any credential material at all — every single time, it re-proves who it is by demonstrating the fact of its existence in the cluster.

The difference is sharpest at pod compromise time. If an attacker gets into a pod holding an ESO-managed secret, they get a value with a full lifetime until the next scheduled rotation — hours, often days. If an attacker gets into a pod using SPIRE, they get an SVID with a TTL bounded by the current window (typically minutes), and extending it further without access to the pod’s own process is impossible — the moment the Workload API stops seeing a legitimate process with the right selectors, reissuance stops.

Federation: identity across clusters and clouds

A trust domain is SPIFFE’s unit of trust — usually one SPIRE deployment with its own root CA. Inside a single trust domain, everything’s simple: the server issues SVIDs, and workloads trust each other through a shared root. Things get harder when services in different clusters or different clouds (that is, different trust domains) need to establish mTLS with each other.

Federation solves this without merging PKIs and without a shared secret between clusters: two SPIRE Servers exchange trust bundles — sets of each other’s trusted root certificates, published via the SPIFFE Federation API. Once federation is configured, a workload in trust domain A can verify an X.509-SVID from a workload in trust domain B exactly as it would verify one from its own domain — because it now has the root that signed the foreign SVID in its bundle. Each cluster still fully controls its own CA and its own registration entries — federation is about exchanging trust, not delegating management.

Comparison table

Dimensionexternal-secrets-operatorSPIFFE/SPIRE
What the pod getsa static secret (API key, password, token)a short-lived SVID — the pod stores nothing
Credential material in the clusteryes, in a Kubernetes Secret (encrypted at rest or not)no — issued on request, never persisted
Root of trustan external secret store (Vault, AWS/GCP SM)SPIRE Server + two-tier attestation
Rotationscheduled, often requires recreating the Secretautomatic, before TTL expiry, no pod restart
On pod compromisesecret with a full lifetime until the next rotationSVID bounded by the current TTL window, not extendable
What’s being proven”I hold the value from the Secret""I’m a process that passed node+workload attestation in this cluster”
Cross-clustera separate secret store or replica per clusterfederation via trust bundle exchange

What it takes to deploy SPIRE and fetch a first SVID

A minimal setup is a SPIRE Server (a StatefulSet, one pod with a disk for the CA key) and a SPIRE Agent (a DaemonSet, one pod per node), plus a single registration entry for a test pod.

The server config sets the trust domain and the node attestation method:

# server.conf (excerpt)
server {
  trust_domain = "example.org"
  data_dir = "/run/spire/data"
  bind_address = "0.0.0.0"
  bind_port = "8081"
}

plugins {
  NodeAttestor "k8s_psat" {
    plugin_data {
      clusters = {
        "prod" = {
          service_account_allow_list = ["spire:spire-agent"]
        }
      }
    }
  }
  KeyManager "disk" { plugin_data { keys_path = "/run/spire/data/keys.json" } }
  DataStore "sql" { plugin_data { database_type = "sqlite3" connection_string = "/run/spire/data/datastore.sqlite3" } }
}

The agent config points at the same trust domain, the server’s address, and a workload attestor for Kubernetes:

# agent.conf (excerpt)
agent {
  data_dir = "/run/spire/data"
  log_level = "INFO"
  server_address = "spire-server"
  server_port = "8081"
  socket_path = "/run/spire/sockets/agent.sock"
  trust_domain = "example.org"
}

plugins {
  NodeAttestor "k8s_psat" { plugin_data { cluster = "prod" } }
  WorkloadAttestor "k8s" { plugin_data { skip_kubelet_verification = true } }
  KeyManager "memory" {}
}

After kubectl apply on both manifests, register a test pod — binding a SPIFFE ID to namespace and service account selectors:

kubectl exec -n spire spire-server-0 -- \
  /opt/spire/bin/spire-server entry create \
  -parentID spiffe://example.org/ns/spire/sa/spire-agent \
  -spiffeID spiffe://example.org/ns/default/sa/checkout-api \
  -selector k8s:ns:default \
  -selector k8s:sa:checkout-api

The pod itself fetches an SVID through the Workload API — a Unix socket the agent mounts on the node via hostPath, and the pod mounts through the same volume:

kubectl run checkout-api -n default --serviceaccount=checkout-api \
  --image=ghcr.io/spiffe/spire-agent:1.10.0 --command -- sleep infinity

kubectl exec -n default checkout-api -- \
  /opt/spire/bin/spire-agent api fetch x509 \
  -socketPath /run/spire/sockets/agent.sock
# Received 1 svid after 12ms
# SPIFFE ID: spiffe://example.org/ns/default/sa/checkout-api

No secret anywhere in the pod manifest — just a service account the agent used to derive selectors, and a socket the pod used to ask “who am I.”

How to verify it works

Confirm the entry is registered and that the trust chain actually closes back to the trust domain’s root:

kubectl exec -n spire spire-server-0 -- \
  /opt/spire/bin/spire-server entry show \
  -spiffeID spiffe://example.org/ns/default/sa/checkout-api
# Entry ID   : 3f1c9b2a-...
# SPIFFE ID  : spiffe://example.org/ns/default/sa/checkout-api
# Parent ID  : spiffe://example.org/ns/spire/sa/spire-agent
# Selector   : k8s:ns:default
# Selector   : k8s:sa:checkout-api
# TTL        : default

kubectl exec -n default checkout-api -- \
  /opt/spire/bin/spire-agent api fetch x509 \
  -socketPath /run/spire/sockets/agent.sock -write /tmp/svid
openssl x509 -in /tmp/svid.0.pem -noout -text | grep -A1 "Subject Alternative Name"
#   URI:spiffe://example.org/ns/default/sa/checkout-api

If entry show finds the registration and the extracted certificate carries the correct SPIFFE ID in its SAN URI, verifiable against the root the SPIRE Server itself issued, the trust chain is closed — from node attestation all the way to a specific pod, with no value ever placed there by hand.

Bottom line

Secrets are what you store and hand out — which means they can be copied, forgotten when it’s time to revoke them, and lost the moment their store is compromised. SPIFFE identity is what a pod proves, from scratch, every single time, by leaning on the platform’s observable state rather than a value planted there in advance. A zero-trust architecture needs both layers — secrets for reaching external systems, identity for trust between your own workloads — but it makes sense to start with the second one: identity never sits in etcd, never ends up in git by accident, and never outlives the pod that received it.


Share this post:

Next Post
SpinKube: running WebAssembly workloads on Kubernetes without a custom runtime