SPIFFE is a CNCF graduated specification for portable, cryptographically verifiable workload identity. SPIRE is its reference implementation. Together they replace shared secrets, bearer tokens, and bootstrap keys with short-lived SVIDs delivered through a local Workload API, giving every service, VM, and pod a real identity it can prove on the wire.
SPIFFE — Secure Production Identity Framework For Everyone — is a CNCF graduated specification that defines a portable, cryptographically verifiable identity for software workloads. A SPIFFE ID looks like spiffe://prod.example/ns/payments/sa/api and is encoded either as a URI in the Subject Alternative Name of an X.509 certificate (an X.509-SVID) or as a claim inside a signed JWT (a JWT-SVID). The format is intentionally boring: it carries a trust domain, a path, and nothing about how the workload was authenticated.
SPIRE is the reference implementation. It runs as a central Server that issues certificates and JWTs, and a per-node Agent that exposes a local Unix-domain Workload API socket to every process on the machine. The Agent attests each calling workload — by Kubernetes ServiceAccount, AWS instance identity document, systemd unit, container image, anything that can be cryptographically tied to the node — and the Server signs an SVID with a short TTL, typically one hour by default.
The point is operational, not philosophical. SPIFFE replaces shared secrets and long-lived bearer tokens with an identity that workloads can prove on the wire, and unifies how that identity is bootstrapped across Kubernetes, plain VMs, and bare metal. It is the substrate for production-grade mTLS and for the workload half of any serious Zero Trust posture.
For most of the last twenty years, the practical answer to "how does service A authenticate to service B?" has been some flavour of shared secret. An API key checked into a config file, a long-lived bearer token mounted as a Kubernetes Secret, a database password baked into an environment variable, a private key generated once and copied across an entire fleet. The mechanism varied; the failure mode did not. Every secret leaks eventually — to a log file, a backup, a CI artifact, a stack trace — and once it is out, the workload that owned it has no way to prove it was not the one that leaked it.
SPIFFE was designed to make that conversation different. Instead of giving workloads a secret to remember, it gives them a name they can prove, an identity that is bound to where the workload is running and what it is, not to a string it happens to hold. The specification, hosted by the CNCF and graduated in 2022, defines what a workload identity looks like (the SPIFFE ID), how it is encoded on the wire (X.509-SVID and JWT-SVID), and how a workload obtains one (the Workload API). SPIRE, also CNCF graduated, is the reference implementation that the spec is read against. There are others — Istio issues SPIFFE-shaped identities natively, Linkerd does too, and a handful of cloud platforms speak the Workload API — but SPIRE is where the production patterns are most clearly visible.
This guide walks through what the spec actually defines, how SPIRE puts it into practice, and where it sits next to your existing PKI.
The spec is short on purpose. It defines an identity format, two SVID formats, and one local API. Everything else — how attestation works, how the issuing CA is operated, how identities are revoked — is left to the implementation. The end-to-end flow that SPIRE realises looks like this.
The Workload API itself is the part most newcomers underestimate. The workload never speaks to the SPIRE Server. It never holds a long-lived credential. It never has a "join token" or a "service account key file" sitting on disk. Its identity is reissued every hour and pushed to it through a stream on a local socket — and if the workload moves, dies, or is restarted somewhere else, the next process to open that socket gets identified on its own merits, not because it inherited a secret.
An operator picks a trust domain name (for example `prod.example`) and stands up a SPIRE Server for it. The Server holds the issuing CA — either a self-signed root or, more commonly, an intermediate signed by an upstream CA — and publishes a trust bundle that any verifier inside the domain can fetch.
A process on a node opens the Unix-domain socket at `/run/spire/agent/api.sock` and calls the Workload API. It does not present credentials. The kernel exposes its PID, UID, GID, and other process metadata to the Agent through `SO_PEERCRED` and `/proc`.
The Agent runs one or more workload attestor plugins against that process — Kubernetes (which pod is this PID in, which ServiceAccount, which container image digest), Docker, systemd unit, Unix UID, parent binary hash. The plugins return a set of selectors. The Agent compares those selectors against the registration entries it received from the Server and decides which SPIFFE ID, if any, this workload is entitled to.
The Agent forwards the request to the Server, which signs an X.509-SVID (a leaf certificate whose SAN URI is the SPIFFE ID) or a JWT-SVID (a signed JWT whose `sub` claim is the SPIFFE ID). The default TTL is one hour for X.509-SVIDs, five minutes for JWT-SVIDs. The Agent caches the result and streams updates back to the workload before expiry.
The workload presents its X.509-SVID for mTLS handshakes (both sides) and uses JWT-SVIDs for cases where mTLS is impractical — async messaging, request-scoped delegation, calls through an L7 gateway that terminates TLS. The trust bundle published by the Server tells every verifier in the trust domain which CA to chain back to.
When two trust domains need to talk — say a staging cluster and a production cluster, or two business units — their Servers exchange signed trust bundles. A workload in `staging.example` presenting an SVID to a workload in `prod.example` is accepted only if the local Server has been told to federate with `staging.example` and a matching bundle has been imported.
A URI of the form `spiffe://
The on-the-wire form of an identity. Two flavours: X.509-SVID (a short-lived certificate with the SPIFFE ID in the SAN URI, used for mTLS) and JWT-SVID (a signed JWT with the SPIFFE ID in `sub`, used where mTLS does not fit). Both are signed by the trust domain's issuing CA.
The set of CA certificates (X.509) and public keys (JWT) that verifiers in a trust domain use to validate SVIDs. Published by the Server, streamed to Agents and workloads through the Workload API, and rotated automatically.
The local Unix-domain socket (`/run/spire/agent/api.sock` by convention) that workloads call. Defined as a gRPC service in the SPIFFE spec, with two RPCs that matter in production: `FetchX509SVID` (returns SVID + trust bundle, streams updates) and `FetchJWTSVID` (returns a JWT for a given audience).
The Agent runs node attestors (proving which node it is — AWS instance identity, GCP instance identity, Kubernetes PSAT, Azure MSI, x509pop) and workload attestors (proving what a calling process is — Kubernetes pod, Docker container, systemd unit, Unix UID). The combination of node + workload attestation is what binds a SPIFFE ID to a running piece of software.
The comparison that matters in most engineering reviews is between SPIFFE/SPIRE and the shared-secret patterns it replaces — a Kubernetes ServiceAccount token mounted as a file, a cloud IAM credential bootstrapped at boot, a static API key in a vault. The two columns look like this.
The columns are not symmetrical. Bearer tokens win on simplicity — there is nothing to attest, no Agent to deploy, no socket to mount, no spec to read. SPIFFE wins everywhere the lifetime, the blast radius, or the portability matters. Most production estates end up running both, with SPIFFE inside the perimeter and bearer tokens for north-south traffic that must traverse third parties.
| Shared secret / SA token | SPIFFE / SPIRE | |
|---|---|---|
| Identity proof | Possession of a bearer string; anyone holding the file is the workload | Cryptographic possession of a key bound to attested process and node identity |
| Lifetime | Days to years; ServiceAccount tokens default to one year unless projected | One hour by default for X.509-SVIDs, minutes for JWT-SVIDs |
| Rotation | Manual or via external automation; often skipped in practice | Continuous, pushed by the Agent before expiry, transparent to the workload |
| Compromise blast | Every request the secret can authenticate, until it is rotated and revoked | At most the remaining TTL on a single SVID, on a single node |
| Portability | Tied to the platform that mints the token (K8s, cloud IAM, vendor) | One identity format across Kubernetes, VMs, bare metal, on-prem, multi-cloud |
| mTLS readiness | None — bearer tokens are application-layer | Native — X.509-SVIDs are valid client and server certificates out of the box |
The cleanest way to convince yourself an SVID is a real X.509 certificate is to inspect one with `openssl`. The SPIFFE ID lives in the SAN as a URI, and nowhere else — the Subject is empty by design.
Two things are worth noticing. The notAfter is one hour away, not one year, and the only Subject Alternative Name is a URI — there is no DNS name, no email, no CN to match against. A traditional TLS verifier expecting a hostname will reject this certificate; verifiers in a SPIFFE deployment are configured to match on the SAN URI instead, via libraries like go-spiffe v2.
# Fetch and inspect a workload's current X.509-SVID
spire-agent api fetch x509 -socketPath /run/spire/agent/api.sock \
-write /tmp/svid
openssl x509 -in /tmp/svid/svid.0.pem -noout -text | grep -A1 'Subject Alternative Name'
# X509v3 Subject Alternative Name:
# URI:spiffe://prod.example/ns/payments/sa/api
openssl x509 -in /tmp/svid/svid.0.pem -noout -dates
# notBefore=Jun 17 09:14:22 2026 GMT
# notAfter =Jun 17 10:14:22 2026 GMT In SPIRE, the operator declares which selectors entitle a workload to which SPIFFE ID through a registration entry. In a Kubernetes deployment, the cleanest way to do that is the SPIRE Controller Manager's `ClusterSPIFFEID` CRD, which generates entries from label selectors as pods come and go.
The CRD does not itself issue anything; it instructs the SPIRE Server to mint registration entries that the Agent, after node attestation, will match against calling pods. The actual node attestation has already happened — typically through `k8s_psat` (Projected ServiceAccount Token), which binds the Agent's identity to the kubelet's view of the node, not to a long-lived join token.
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterSPIFFEID
metadata:
name: payments-api
spec:
# SPIFFE ID template — interpolated per matching pod
spiffeIDTemplate: "spiffe://prod.example/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}"
# Which pods this entry covers
podSelector:
matchLabels:
app.kubernetes.io/name: payments-api
app.kubernetes.io/component: api
# Restrict to a single namespace
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: payments
# SVID lifetime — one hour is the SPIRE default
ttl: 1h
# Optional: federate with another trust domain
federatesWith:
- "staging.example" The go-spiffe v2 library hides almost everything. The workload opens an `X509Source` against the Workload API, hands it to the TLS config, and never thinks about certificates again — rotation, trust bundle refresh, and federation all happen in the background.
The same pattern in reverse — `tlsconfig.MTLSClientConfig` plus `AuthorizeID(...)` — gives you a client that only talks to a specific SPIFFE ID. Identity-aware authorisation, in other words, becomes a single line of configuration; no certificate parsing, no SAN matching, no manual chain validation.
package main
import (
"context"
"log"
"net/http"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
"github.com/spiffe/go-spiffe/v2/workloadapi"
)
func main() {
ctx := context.Background()
// Open a streaming source against the local Workload API.
// The library connects to /run/spire/agent/api.sock by default
// and keeps the SVID + trust bundle up to date in memory.
source, err := workloadapi.NewX509Source(ctx)
if err != nil {
log.Fatalf("workload API: %v", err)
}
defer source.Close()
// Only accept peers whose SPIFFE ID lives in this trust domain.
td := spiffeid.RequireTrustDomainFromString("prod.example")
tlsCfg := tlsconfig.MTLSServerConfig(source, source, tlsconfig.AuthorizeMemberOf(td))
server := &http.Server{
Addr: ":8443",
TLSConfig: tlsCfg,
Handler: http.HandlerFunc(handle),
}
log.Fatal(server.ListenAndServeTLS("", ""))
}
func handle(w http.ResponseWriter, r *http.Request) {
// r.TLS.PeerCertificates[0] carries the caller's SPIFFE ID in the SAN URI.
w.Write([]byte("ok\n"))
} The strength of a SPIFFE deployment is not in the certificates it issues — those are routine X.509 — but in the attestation that decides who gets which one. A SPIRE Server configured with the legacy `k8s_sat` node attestor accepts any caller that holds a ServiceAccount token, which is the same shared secret SPIFFE was meant to eliminate. The PSAT (`k8s_psat`) attestor binds attestation to a projected, audience-scoped, short-lived token that the kubelet refreshes; on cloud nodes, use the platform's instance-identity attestor (`aws_iid`, `gcp_iit`, `azure_msi`). Workload attestation should pin container image digest, not name, and ServiceAccount, not just namespace. The blast radius of a misconfigured attestor is the entire trust domain.
A one-hour SVID is a feature, not a problem — but only if the workload re-reads it before it expires. Libraries like go-spiffe handle this transparently because they stream from the Workload API. Hand-rolled clients that fetch once at startup and reuse the certificate are a common failure mode: they work fine in staging, then fail simultaneously across the fleet exactly one hour after deployment. Treat any code path that touches an SVID file as a bug; SVIDs should live in memory, refreshed by the library.
The SPIRE Server is the issuing CA for the trust domain. If it is down, no new SVIDs are issued; existing ones keep working until their TTL runs out. Run the Server in HA mode (multiple replicas sharing a SQL or PostgreSQL-backed datastore), and treat the issuing key the same way you treat any CA key — back it by an HSM, rotate it on a schedule, and plan a trust-bundle rollover sequence that overlaps old and new bundles so that no verifier briefly sees an unknown signer.
Federation is how SPIFFE solves the multi-domain case — staging talking to prod, on-prem talking to a cloud landing zone, one business unit talking to another. It works by exchanging signed trust bundles between Servers and configuring each side's verifiers to accept the other's SPIFFE IDs. The mechanism is straightforward; the policy is not. Federation makes a remote trust domain's compromise your problem. Treat each `federatesWith` entry as a deliberate, documented decision, with a clear answer to "what happens when their Server is breached?".
By default a SPIRE Server can self-sign its issuing CA, which is convenient for demos and useless in production. In any organisation that already runs a corporate PKI, SPIRE should be configured with an upstream-signed CA: SPIRE generates an intermediate CSR, the corporate root or an offline issuing CA signs it, and SPIRE issues SVIDs from that intermediate. Verifiers outside the SPIFFE world (load balancers, legacy services, partners) then chain back to the same root they already trust, and your certificate inventory covers the workload identities alongside everything else.
Upstream CA for SPIRE — Evertrust PKI signs the intermediate that SPIRE uses to issue SVIDs, so workload identities chain back to the same governed, audited root as the rest of your TLS and client certificates. Key generation can be HSM-backed, the intermediate's policy and lifetime are enforced centrally, and rotating the SPIRE issuing key becomes a routine intermediate-CA operation rather than a bespoke procedure.
Visibility at workload scale — Evertrust CLM ingests the certificates SPIRE issues alongside the long-lived certificates protecting servers, load balancers, and devices, so a single inventory covers the millions of one-hour SVIDs a busy cluster mints per day and the conventional certificates renewed on a calendar. Outage prevention, expiry monitoring, and discovery work the same on both sides.
One policy across human and machine — the same governance model that approves a new code signing certificate or a partner mTLS profile also approves a new SPIFFE trust domain, a federation link, or an attestor configuration. Two operational surfaces — long-lived corporate PKI and short-lived workload identity — converge on one policy, one audit trail, and one source of truth.