Workload identity with SPIFFE
Every pod on your clusters gets a SPIFFE identity and a certificate that renews itself. Use it to let your services prove who they are to each other — no passwords, no shared secrets, nothing to rotate by hand.
What you get
Your organization's platform runs a SPIRE server; every cluster that joins the platform runs a SPIRE agent on each node. Together they hand every pod an identity — a SPIFFE ID — and short-lived credentials that prove it:
spiffe://o-jpqv6bfnj0/ns/payments/sa/api
└ trust domain └ namespace └ service account
- The trust domain is your organization's id (
o-…). It never changes, even if you rename the organization. A second platform you run gets its own, the cluster's id (cl-…). - The rest is the pod's namespace and service account. Two pods with the same namespace and service account have the same identity — on one cluster or on five clusters of the same platform.
- An X.509 certificate (X.509-SVID), valid for 4 hours and renewed long before that, for mTLS.
- A JWT (JWT-SVID) on request, valid for 1 hour, for calls where a certificate does not fit — an HTTP header, a queue message.
Nothing to install: the agent and the SPIFFE CSI driver are part of the platform on every cluster that joined it.
Use it in your app
1. Mount the Workload API
The agent serves the SPIFFE Workload API on a local socket. Mount it with the CSI driver and tell your code where it is:
apiVersion: apps/v1
kind: Deployment
metadata: { name: api, namespace: payments }
spec:
template:
spec:
serviceAccountName: api # → spiffe://<org>/ns/payments/sa/api
containers:
- name: api
image: registry.example.com/payments/api:1.4.0
env:
- name: SPIFFE_ENDPOINT_SOCKET
value: unix:///spiffe-workload-api/spire-agent.sock
volumeMounts:
- name: spiffe-workload-api
mountPath: /spiffe-workload-api
readOnly: true
volumes:
- name: spiffe-workload-api
csi:
driver: csi.spiffe.io
readOnly: true
No secret is mounted: the socket hands out credentials only to the pod asking, after the agent checked which pod it is.
2. Talk mTLS, authorize by SPIFFE ID
With the official Go library, go-spiffe, a server that accepts only one caller looks like this:
source, err := workloadapi.NewX509Source(ctx) // reads SPIFFE_ENDPOINT_SOCKET
if err != nil { log.Fatal(err) }
defer source.Close()
// only the checkout service may call us
checkout := spiffeid.RequireFromString("spiffe://o-jpqv6bfnj0/ns/shop/sa/checkout")
srv := &http.Server{
Addr: ":8443",
TLSConfig: tlsconfig.MTLSServerConfig(source, source, tlsconfig.AuthorizeID(checkout)),
}
log.Fatal(srv.ListenAndServeTLS("", ""))The source keeps the certificate fresh in the background; you never handle a file or a renewal. Authorize by exact ID, or by a set of them — not by “anything in my trust domain”, which would let every pod in your organization in.
Any other language
There are SPIFFE libraries for Java and Python too. For everything else — Node, Ruby, an off-the-shelf proxy or database — run spiffe-helper as a sidecar: it writes the certificate, key and trust bundle to files in a shared volume and renews them, so anything that reads PEM files can do mTLS. It needs the same CSI volume as above.
Locally, before a cluster
The Workload API exists only on a cluster. Keep the identity at the edge of your code — one function that returns a TLS config — and on a laptop make it plain TLS or no TLS with a flag. Then test the real thing on a small dev cluster: build one in the cabinet, put it on a schedule so it sleeps at night, and deploy to it the same manifests you ship.
Run it for your customers
How you split identities is how you split trust. A few rules that hold up:
- One service account per service.
ns/payments/sa/apiandns/payments/sa/worker, never both ondefault. - One namespace per tenant when tenants must not reach each other.
ns/acme/sa/apiandns/globex/sa/apiare different identities, so a policy can tell Acme's pods from Globex's — and a mistake in one tenant's config cannot borrow the other's identity. - Write the allowed callers down next to the service, as SPIFFE IDs, and keep that list in the same repository as its manifests. A reviewer can read who may call what.
- Many clusters, one identity. A service deployed to every cluster of a platform has the same ID everywhere, so a client in one cluster can call it in another with no extra setup. If you need to tell clusters apart, put the difference in the namespace (
ns/payments-eu/…) or register a custom ID (below). - Separate platforms do not trust each other. Each platform has its own trust domain and there is no federation between them. That is a feature when a platform is a customer's isolated environment; it is a wall when you expected traffic to cross it.
Custom IDs
The default template is spiffe://<trust domain>/ns/<namespace>/sa/<service account>. When you need a different shape — a tenant or an environment in the path — add a ClusterSPIFFEID. It is cluster-scoped, so an owner or admin creates it, and it must name the platform's class, or SPIRE ignores it:
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterSPIFFEID
metadata:
name: tenants
spec:
className: spire-spire # required: the platform's registrar reads only this class
spiffeIDTemplate: "spiffe://{{ .TrustDomain }}/tenant/{{ index .PodMeta.Labels \"tenant\" }}/{{ .PodSpec.ServiceAccountName }}"
podSelector:
matchExpressions:
- { key: tenant, operator: Exists }
A pod matched by your own entry gets your ID; the default is a fallback for every pod nothing else matched. Keep your templates to labels you control — a label anyone can set on a pod is an ID anyone can claim.
What it does not do yet
- No public JWT issuer. The JWT-SVIDs' discovery endpoint answers only inside the platform cluster, so AWS, GCP or a third-party API cannot be told to trust them yet.
- No Vault login by identity for your apps. The platform's Vault uses SPIFFE for its own pieces; reading your secrets by SVID is not offered yet.
- Nothing enforces mTLS for you. No service mesh is installed; your services choose to use their identities. Network policies still apply as usual.
See it working
- Cluster page › Workload identity on the platform cluster: how many clusters and agents report to SPIRE, how many are live, and when its certificate authority expires. Read every 15 minutes.
- The SPIRE door on the platform cluster (
spire-ui.pub.<your domain>): a read-only view of the server, its agents and its entries. Owners and admins only. - A cluster with no live agent is named on that card — its pods get no identity until the agents run there again.
- Agents:
rockslab_get_spire_statusin the Rockslab MCP gives the same, cluster by cluster.
Troubleshooting
| What you see | Why | What to do |
|---|---|---|
The pod waits on FetchX509SVID forever | No CSI volume, or no agent on its node | Check the mount above; check the cluster has live agents (Workload identity card). |
The ID ends in /sa/default | The pod has no service account of its own | Set serviceAccountName. |
| No identity on a new cluster | It has not joined the platform yet, or the platform has no domain | Wait for the join to finish; a new cluster gets an hour's grace before it is reported. |
| A client is refused with a certificate error | The server authorizes a different ID | Compare the exact SPIFFE ID the client has with the one the server allows. |
Your ClusterSPIFFEID does nothing | It has no className: spire-spire | Add it. |