Technical Guide Advanced 13 min read

Workload Identity with SPIFFE and SPIRE

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.

Quick Facts

Type
Technical Guide
Level
Advanced
Next
Client Authentication Certificates

Overview

SPIFFESecure 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.

What SPIFFE actually defines

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.

1

Trust domain declared

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.

2

Workload calls the local Workload API

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`.

3

Agent attests the workload

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.

4

Server signs an SVID

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.

5

Workload uses the SVID

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.

6

Federation across trust domains

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.

Key concepts

SPIFFE ID

A URI of the form `spiffe:///`. The trust domain is the security boundary; the path is whatever the operator finds useful — a namespace and service account in Kubernetes (`/ns/payments/sa/api`), an AWS account and instance role, a logical service name. SPIFFE IDs are case-sensitive and intentionally opaque to outsiders.

SVID

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.

Trust bundle

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.

Workload API

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).

Attestor plugins

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.

SPIFFE vs typical workload auth

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 tokenSPIFFE / SPIRE
Identity proofPossession of a bearer string; anyone holding the file is the workloadCryptographic possession of a key bound to attested process and node identity
LifetimeDays to years; ServiceAccount tokens default to one year unless projectedOne hour by default for X.509-SVIDs, minutes for JWT-SVIDs
RotationManual or via external automation; often skipped in practiceContinuous, pushed by the Agent before expiry, transparent to the workload
Compromise blastEvery request the secret can authenticate, until it is rotated and revokedAt most the remaining TTL on a single SVID, on a single node
PortabilityTied to the platform that mints the token (K8s, cloud IAM, vendor)One identity format across Kubernetes, VMs, bare metal, on-prem, multi-cloud
mTLS readinessNone — bearer tokens are application-layerNative — X.509-SVIDs are valid client and server certificates out of the box

What an X.509-SVID looks like on disk

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

Registering a workload

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.

SPIRE Server registration entry — Kubernetes workload yaml
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"

A Go workload that fetches and rotates an SVID

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.

Go workload — fetch and rotate an X.509-SVID go
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"))
}
SPIFFE is a specification, not a product. The Workload API contract is the same across SPIRE, Istio's istiod, Linkerd's identity controller, and a handful of cloud platforms — but their attestation models, their CA configurations, and their operational footprints are not interchangeable. If you already run a service mesh that issues SVIDs to every pod, running SPIRE alongside it usually means two issuing CAs, two trust bundles, and two sources of truth for the same identities. Pick one issuer per trust domain, and let the others consume.

Production considerations

Attestation is the whole game

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.

Short TTLs need supervisor logic

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.

Server HA and trust-bundle continuity

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 across boundaries

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?".

Upstream-signed CA — plug into your existing PKI

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.

How we help

Evertrust & Workload Identity with SPIFFE and SPIRE

Upstream CA for SPIREEvertrust 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 scaleEvertrust 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 machinethe 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.