SPIFFE and SPIRE give every Linux workload a short-lived, cryptographically verifiable identity (an SVID) that replaces long-lived API keys, bearer tokens, and bootstrap secrets in service-to-service authentication. SPIFFE is the specification: a URI format (spiffe://trust-domain/path) plus the SVID envelope. SPIRE is the reference implementation, with a server that mints SVIDs and a per-node agent that attests workloads and delivers their identities through a local Workload API. Together they let you do mTLS, JWT-bound RPCs, and database authentication without a single static credential on disk.
SPIFFE defines the identity format (the SPIFFE ID URI and the SVID); SPIRE is the CNCF graduated reference implementation that issues and rotates those identities.
SVIDs come in two flavours: X.509-SVIDs for mTLS (default TTL one hour) and JWT-SVIDs for HTTP APIs or database auth.
Workloads never see a bootstrap secret. They call a local UDS-based Workload API, gated by kernel-level attestation (UID, cgroup, container labels).
SPIRE 1.10 (March 2026) ships PSAT-only Kubernetes attestation, native PostgreSQL plugin storage, and stable federation over the SPIFFE Federation API.
The right threat model question is "what breaks first if the agent is compromised?" The answer: only workloads on that single node, because every node has its own attestation chain.
SPIRE works equally well outside Kubernetes. Bare-metal Linux nodes attest via TPM, AWS IID, Azure MSI, or join tokens.
Why workload identity replaces long-lived secrets
Every secret-management war story I've sat through reduces to the same pattern. A long-lived credential ended up somewhere it wasn't supposed to be: a CI log, a backup tarball, a Slack DM, a hard-coded fallback in a Helm chart. The blast radius of a leaked API token is "every system that trusts it, for as long as it remains valid." Rotating it means hunting down every consumer. That's the failure mode workload identity is built to eliminate.
SPIFFE (the Secure Production Identity Framework for Everyone) replaces "the workload knows a secret" with "the workload is a verifiable identity." A service in payments-prod doesn't authenticate by presenting POSTGRES_PASSWORD. It presents an X.509 certificate whose Subject Alternative Name is spiffe://payments.prod.example.com/api/checkout, signed by a CA the database trusts, valid for the next 47 minutes. There's nothing on disk for an attacker to steal that wouldn't have rotated by the time they exfiltrated it.
Honestly, that changes the default question I ask during design reviews. Instead of "where do we store this secret?" it becomes "what attests this workload's identity, and what does a compromise of that attestor get you?" The answer is usually one node, for one rotation window. Compare that to a static service account JSON key that grants production-wide access until somebody notices.
What is the difference between SPIFFE and SPIRE?
SPIFFE is the spec. SPIRE is one implementation of it (Istio's istiod CA is another, as is cert-manager's CSI driver). Knowing which is which matters because portability flows through SPIFFE, not SPIRE.
SPIFFE defines three things: the SPIFFE ID URI (spiffe://trust-domain/path), the SVID (SPIFFE Verifiable Identity Document, in both X.509 and JWT shapes), and the Workload API, a gRPC service over a Unix Domain Socket that workloads call to fetch their identity. The full spec lives at the SPIFFE project documentation.
SPIRE is the reference implementation that runs as two binaries: spire-server (one per trust domain, holds the CA and the registration database) and spire-agent (one per node, runs the Workload API and handles attestation).
If you swap SPIRE for another SPIFFE-compliant issuer later, your workloads don't change. They're still calling /run/spire/agent.sock for an SVID. That decoupling is the whole point.
SPIRE architecture on a Linux host
Picture three layers. At the top is the SPIRE Server, typically three replicas behind a load balancer, fronting a PostgreSQL or SQLite datastore. It owns the trust domain's signing key (which lives in a KMS, TPM, or HSM in any environment I'd sign off on). Below it, on every Linux node that runs workloads, is a single spire-agent process. The agent attests itself to the server (proving "I am node ip-10-0-1-42 in the prod account") and receives a node SVID. It then exposes the Workload API as a UDS at /run/spire/agent.sock.
At the bottom are the workloads: your services, sidecars, daemons. They call the Workload API. The agent looks at the calling process's PID, walks /proc/[pid] and the cgroup hierarchy to pull selectors (UID, GID, container image hash, Kubernetes pod label, systemd unit name), and matches those against registration entries the server has pre-loaded. If selectors match, the workload gets an SVID. If they don't, it gets PermissionDenied. There is no token, no shared secret, no environment variable in this flow, only kernel-truth selectors.
The diagram in prose: SPIRE Server (with HSM-backed CA) ↔ mTLS ↔ SPIRE Agent (per node) ↔ UDS ↔ Workload, where the UDS hop is the only one without TLS because the kernel itself is the access control.
Installing SPIRE Server and Agent on Linux
I'll walk through a non-Kubernetes install on Ubuntu 24.04, because most SPIRE tutorials assume k8s and that hides the underlying mechanics. SPIRE 1.10.0 was released in March 2026; binaries are published on the official SPIRE GitHub releases page.
On each workload node, install the agent and bootstrap it with a one-time join token the server mints:
# On the server: mint a join token tied to a future agent SPIFFE ID
sudo spire-server token generate \
-spiffeID spiffe://prod.example.com/agent/node-42
# Copy the printed token, then on the agent node:
sudo mkdir -p /etc/spire/agent /var/lib/spire/agent /run/spire
sudo tee /etc/spire/agent/agent.conf >/dev/null <<'EOF'
agent {
data_dir = "/var/lib/spire/agent"
log_level = "INFO"
server_address = "spire-server.prod.example.com"
server_port = "8081"
socket_path = "/run/spire/agent.sock"
trust_domain = "prod.example.com"
trust_bundle_path = "/etc/spire/agent/bootstrap.crt"
insecure_bootstrap = false
}
plugins {
NodeAttestor "join_token" { plugin_data {} }
KeyManager "memory" { plugin_data {} }
WorkloadAttestor "unix" {
plugin_data { discover_workload_path = true }
}
WorkloadAttestor "systemd" { plugin_data {} }
}
EOF
sudo spire-agent run -config /etc/spire/agent/agent.conf \
-joinToken "<TOKEN_FROM_SERVER>"
The agent attests itself once, the join token is burned, and from that point on the agent re-attests using its node SVID at every renewal. (I hit this exact bootstrap when I rolled SPIRE out across a 30-node fleet last year, and forgetting to set insecure_bootstrap = false was the one mistake worth catching in code review.)
How does SPIRE attest workloads?
This is the part that throws people off coming from token-based systems. There's no enrollment ceremony for each workload, no "register service X with secret Y." Instead, you tell the server: any process that satisfies these selectors gets this SPIFFE ID. The agent enforces the selectors using kernel-level facts the calling process cannot lie about.
Selectors for the unix attestor include unix:uid, unix:gid, unix:supplementary_gid, unix:path (with SHA256 of the binary), and unix:sha256 of the binary content. For systemd workloads you get systemd:id. In containers you get docker:label, k8s:pod-label, k8s:container-image. None of these are presented by the workload. They are read from /proc by the agent using the SO_PEERCRED of the UDS connection.
Register the checkout service to run as UID 1001 from a specific binary hash:
# From the server, register the workload entry
sudo spire-server entry create \
-parentID spiffe://prod.example.com/agent/node-42 \
-spiffeID spiffe://prod.example.com/api/checkout \
-selector unix:uid:1001 \
-selector unix:sha256:b3d2...e9f1 \
-ttl 3600
# Test from the workload's user account on node-42:
sudo -u checkout spire-agent api fetch x509 \
-socketPath /run/spire/agent.sock \
-write /tmp/svid
If the calling process is UID 1001 and its /proc/[pid]/exe hashes to the registered value, the agent writes svid.pem, svid_key.pem, and bundle.pem to /tmp/svid. Otherwise it returns nothing. A different binary running as the same UID? Denied, the hash differs. The same binary running as a different UID? Denied, the selector chain fails.
Using X.509-SVIDs for mTLS between Go services
Once a workload has an SVID, mTLS is essentially free. The go-spiffe library wires the Workload API into a *tls.Config that auto-rotates as new SVIDs are pushed. Here is a complete server that requires clients with a matching trust domain:
package main
import (
"context"
"fmt"
"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()
// Connect to the local SPIRE Workload API
source, err := workloadapi.NewX509Source(ctx,
workloadapi.WithClientOptions(
workloadapi.WithAddr("unix:///run/spire/agent.sock")))
if err != nil {
log.Fatalf("workload API: %v", err)
}
defer source.Close()
// Only accept clients whose SPIFFE ID is in our trust domain
td := spiffeid.RequireTrustDomainFromString("prod.example.com")
tlsCfg := tlsconfig.MTLSServerConfig(source, source,
tlsconfig.AuthorizeMemberOf(td))
srv := &http.Server{
Addr: ":8443",
TLSConfig: tlsCfg,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// The peer's verified SPIFFE ID is in the cert SAN
peer := r.TLS.PeerCertificates[0].URIs[0].String()
fmt.Fprintf(w, "hello %s\n", peer)
}),
}
log.Fatal(srv.ListenAndServeTLS("", ""))
}
No certificate files are passed to ListenAndServeTLS; the empty strings tell Go to use the dynamic config. When the SVID rotates 50 minutes from now, the next handshake uses the new cert with zero downtime and zero reload. That property (silent rotation) is what makes one-hour TTLs operationally tolerable.
JWT-SVIDs for HTTP APIs and database auth
mTLS is great between services you control. Sometimes, though, you need to authenticate to something that speaks HTTP bearer tokens or to a database whose connector wants a password. JWT-SVIDs cover that case. A JWT-SVID is a signed JWT whose sub claim is a SPIFFE ID and whose aud claim is the intended recipient.
# Fetch a JWT-SVID for a specific audience
spire-agent api fetch jwt \
-socketPath /run/spire/agent.sock \
-audience postgres://payments-db.prod.example.com
The receiving side validates the JWT against the SPIFFE trust bundle's JWKS endpoint, checks the audience matches, and authorises based on the sub claim. PostgreSQL 17 supports this via the pgjwt extension or, more cleanly, by configuring the database to accept a SPIFFE-issued client cert (X.509-SVID) over TLS with cert auth in pg_hba.conf. For the database side of that conversation see our PostgreSQL hardening guide for pg_hba and TLS; the SPIFFE-issued cert slots straight into the clientcert=verify-full path.
The default JWT TTL in SPIRE is five minutes precisely because JWTs are bearer tokens: anyone who steals one in flight can replay it. Five minutes is a tradeoff, long enough to survive clock skew and request retries, short enough that exfiltration-and-replay is rarely worth the attacker's time.
Federating trust domains across clusters
One trust domain per blast-radius boundary is the rule I apply. A staging cluster and a prod cluster should never share a trust domain, because a compromise in staging would otherwise mint identities accepted in prod. Same for separate business units, separate cloud accounts, or anywhere the answer to "who controls the CA?" is different.
To let services in different trust domains talk to each other, SPIFFE defines federation: each trust domain publishes its current trust bundle (CA roots, JWT signing keys) at a discoverable HTTPS endpoint, and the peer SPIRE Server fetches and pins it. SPIRE 1.10 implements the SPIFFE Federation specification with automatic bundle refresh.
# On the prod SPIRE Server, federate with the data-eng trust domain
sudo spire-server federation create \
-trustDomain data.example.com \
-bundleEndpointURL https://spire.data.example.com/bundle \
-bundleEndpointProfile https_spiffe \
-endpointSpiffeID spiffe://data.example.com/spire/server
# Register a workload that's allowed to call across the federation
sudo spire-server entry create \
-parentID spiffe://prod.example.com/agent/node-42 \
-spiffeID spiffe://prod.example.com/api/checkout \
-selector unix:uid:1001 \
-federatesWith spiffe://data.example.com
The checkout service can now mTLS-handshake with a service in data.example.com; both sides validate against bundles they pulled over a SPIFFE-authenticated channel. The federation table is the new firewall: explicit, auditable, and revocable in one command.
Threat model: what breaks first?
I always run this exercise before signing off on a workload-identity rollout. So, walk the attack tree from the outside in and ask what an attacker gains at each step.
Compromise of one workload process. Attacker gets that workload's current SVID, valid until the next rotation (≤1 hour for X.509, ≤5 min for JWT). They cannot mint new SVIDs. They cannot impersonate sibling workloads with different selectors. Blast radius: that workload, that window.
Compromise of a SPIRE Agent on one node. Attacker can fetch SVIDs for every workload registered to that agent's parent ID. Critical point: they cannot mint SVIDs for workloads on other nodes because each node has its own attested parent SPIFFE ID. Blast radius: one node's workloads, one rotation cycle until you revoke the agent's node entry.
Compromise of the SPIRE Server host. If the signing key is HSM/KMS-backed, attacker can sign new SVIDs while they hold the host but cannot exfiltrate the key. If the key is on disk, the entire trust domain is forfeit and you rotate the root. This is why I refuse disk key managers in production.
Compromise of a federated peer's trust domain. They can mint identities your workloads will accept on cross-domain calls, but only on calls explicitly marked federatesWith. Blast radius is bounded by your federation table, not by the size of the breach.
Compare each of those to the equivalent failure modes with long-lived secrets and the case writes itself. A leaked POSTGRES_PASSWORD is valid until somebody rotates it, accepted from anywhere on the network, and indistinguishable from legitimate use in audit logs. A leaked SVID is valid for the next 47 minutes, tied in the cert SAN to a specific SPIFFE ID, and trivially correlatable to its rotation lineage. For the broader picture of removing static secrets from your fleet, see our Linux secrets management with Vault and SOPS; SPIRE is the piece that retires the "service account" category entirely. And SPIRE-issued mTLS pairs naturally with the network segmentation in our zero trust networking on Linux with WireGuard walkthrough: identity at L7, segmentation at L3/L4.
Frequently Asked Questions
Is SPIFFE the same as mTLS?
No. SPIFFE is an identity framework that specifies how a workload proves who it is. mTLS is a transport mechanism. SPIFFE's X.509-SVIDs make mTLS dramatically easier because both sides get verifiable identities without manually managing certificates, but you can also use SPIFFE JWT-SVIDs over plain HTTPS or for database authentication where mTLS isn't practical.
Does SPIRE require Kubernetes?
No. SPIRE runs on any Linux host. The unix and systemd workload attestors cover bare-metal and VM deployments, and node attestors exist for AWS IID, GCP IIT, Azure MSI, TPM 2.0, and one-time join tokens. Kubernetes is one supported environment among many.
How are SVIDs rotated?
The SPIRE Agent rotates X.509-SVIDs at half their TTL by default. For a 1-hour TTL it requests a new SVID at 30 minutes. Workloads using the Workload API receive the new SVID via a streaming gRPC update with no restart needed. JWT-SVIDs are fetched on demand and are short-lived (default 5 minutes), so they're effectively rotated per request.
Can SPIRE federate across clusters or clouds?
Yes. Each SPIRE Server publishes its trust bundle at a SPIFFE-authenticated HTTPS endpoint, and peer servers fetch and pin it. Workloads must be explicitly registered with federatesWith to accept SVIDs from a foreign trust domain, which keeps cross-cluster access auditable and minimum-privilege.
What happens if the SPIRE Server is offline?
Workloads keep functioning with their currently issued SVIDs until those SVIDs expire, typically up to one hour. The agent caches the server's CA bundle and continues serving the Workload API. New workload registrations and SVID rotations stop until the server returns, so design SPIRE Server for HA with at least three replicas and a clustered datastore.
How Linux kernel live patching works in 2026 with kpatch, Ubuntu Livepatch, and TuxCare KernelCare. Install commands, comparison table, and audit tips for zero-downtime CVE fixes.
Wolfi vs Distroless vs Alpine base images for 2026 Linux workloads, with CVE benchmarks, apko and cosign pipeline snippets, and real migration examples for Node.js and Python services.
Deploy Keylime continuous remote attestation on Linux with TPM 2.0: registrar, verifier, Rust agent, IMA runtime policies, and revocation webhooks in 2026.