Every mutual TLS connection in a mesh rests on each side proving who it is, and the thing it proves it with is an SVID, a SPIFFE Verifiable Identity Document. This post starts with the SVID itself, then widens to the two systems around it: SPIRE, the SPIFFE Runtime Environment that can issue these identities, and WIMSE, the IETF effort (Workload Identity in Multi System Environments) that standardises how they travel across HTTP. The names are a mouthful; the ideas are small, and worth getting exactly right.
This is a primer, so I am going to build it up from the bottom: what an SVID is, the one piece of data it wraps, the two formats it comes in, how a workload is handed one without ever holding a secret, and the one rule about naming that saves you a painful migration later. Then I will show where it all lands in Solo Enterprise for Istio, because that is where I see most people meet SVIDs for the first time, usually without realising the certificate their proxy is presenting is one.
What an SVID actually is
An SVID, a SPIFFE Verifiable Identity Document, is the document with which a
workload proves its identity to whatever it is talking to. It carries exactly one
identity, a SPIFFE ID, wrapped in a
cryptographically verifiable container. And it is only valid if it was signed by
an authority inside that SPIFFE ID's own trust domain. That last clause is the
whole security model in one line: a document claiming to be
spiffe://acme.com/payments means nothing unless it carries a
signature that chains back to acme.com's trust root.
So there are two separate things, and keeping them apart makes everything else clearer. The SPIFFE ID is the name. The SVID is the signed envelope that name travels in. People say "SVID" when they mean the cert and "SPIFFE ID" when they mean the string inside it, and they are right both times, they are just naming different layers of the same object.
Anatomy of a SPIFFE ID
A SPIFFE ID is just a URI, RFC 3986 compliant, with three parts:
spiffe://acme.com/ns/staging/sa/payments scheme trust domain path
- Scheme is always
spiffe://. Nothing else is a valid SPIFFE ID. - Trust domain is the authority component of the URI (for example
acme.com). It identifies the system's root of trust, the keys that every SVID in the domain is verified against; the authority is the name, the root of trust is what it belongs to. - Path identifies one specific workload inside that trust domain. Its meaning is yours to define; the spec only constrains its shape.
The path is hierarchical, like a filesystem path, and the most common convention
you will see in Kubernetes maps it straight onto the workload's service account:
/ns/<namespace>/sa/<serviceaccount>. That is exactly the
shape Istio uses out of the box, and the one the SPIRE templates further down
generate. There is real value in that convention: the identity is derived from
something the platform already manages and already governs.
Two formats: X.509-SVID and JWT-SVID
The same SPIFFE ID can be wrapped in one of two containers, and the choice between them is not stylistic. It comes down to whether anything sits between the two workloads.
| X.509-SVID | JWT-SVID | |
|---|---|---|
| How the ID is carried | In an X.509 certificate, in the URI SAN field. | As the subject of a signed JWT. |
| Proven by | Possession of the matching private key during the TLS handshake. | Presenting the token as a bearer credential. |
| Use it for | The default. Direct workload-to-workload mTLS. | When a proxy or load balancer sits between the two workloads and the TLS connection cannot survive the hop. |
| Main weakness | None for this job; it is the recommended format. | Replayable. A copied bearer token can be reused until it expires. |
The SPIFFE spec is blunt about the order of preference: use an X.509-SVID whenever you can, and reach for a JWT-SVID only when something in the path terminates the connection so the certificate identity cannot reach the far end. A bearer token can be copied and replayed in a way a private key bound to a TLS session cannot, so the X.509 form is structurally safer and the JWT form exists to solve a problem the X.509 form genuinely cannot. Hold on to that distinction; it is the hinge the whole "where the cert stops carrying identity" section turns on.
How a workload gets issued one
Here is the part that surprises people. A workload does not generate its own SVID, and it does not start life knowing its own identity. It asks a local endpoint, the SPIFFE Workload API, usually over a Unix domain socket, and gets back everything it needs:
- its SPIFFE ID,
- a freshly minted, short-lived X.509-SVID and the private key that goes with it,
- the trust bundle, the set of root keys it needs to verify the SVIDs other workloads present to it.
Two properties make this work, and both matter. First, the calling workload needs no prior knowledge of its own identity and no auth token to make the call. It does not authenticate to the Workload API with a secret, because there is no secret to pre-place. Instead the local agent works out what the caller is by inspecting it, its process, the node it runs on, the orchestrator metadata, a step called attestation, and issues an identity that matches. Second, the private keys are short lived and rotated automatically, so there is no long-lived credential sitting on disk waiting to leak. You never copy a key into a workload, which means you never have a key to rotate by hand or forget to revoke.
The thing that makes SPIFFE identity strong is the thing it removes: there is no bootstrap secret. Identity comes from attesting what the workload is, not from what it was handed at deploy time.
The three issuance models in a mesh
"A workload asks the Workload API" is the clean version. In a real Istio mesh it plays out in one of three ways, and the differences are worth knowing because they decide how much you mount into every pod.
| Model | Who attests | What the pod has to carry |
|---|---|---|
| Default Istio, no SPIRE | The Kubernetes service account token vouches for the pod; istiod signs the cert. | Nothing extra. The proxy gets its cert from istiod, the mesh CA, ztunnel in ambient or the Envoy sidecar in sidecar mode. The cert is a SPIFFE-format X.509-SVID in all but who issued it. |
| Sidecar + SPIRE | The SPIRE agent attests the pod (node plus workload selectors), then issues the SVID. | Each pod mounts the SPIRE agent socket, via the SPIFFE CSI driver, so it can call the Workload API itself. A socket or volume in every workload. |
| Ambient + SPIRE | The SPIRE agent attests the pod, with ztunnel acting as its delegate. | Nothing in the pod. ztunnel fetches the SVID on the pod's behalf. No socket mounts, no volume changes in workloads. |
The ambient model is the one I find most elegant, and it is the part that made the rest click for me, so it is worth drawing. In Solo's ambient distribution, ztunnel on each node acts as a trusted delegate of the local SPIRE agent using the SPIRE DelegatedIdentity API. When a workload is enrolled in the mesh, ztunnel reads the workload's PID, asks the SPIRE agent to attest it, and on success receives that workload's X.509-SVID to use for its mTLS connections. The pod itself never touches a socket.
So where does that SPIRE agent actually live? SPIRE has two parts. The SPIRE server is central, one logical instance per trust domain, and it holds the CA and signing keys and does the actual signing; nothing in the data path talks to it directly. The SPIRE agent runs as a DaemonSet, one pod per node, and exposes the Workload API over a Unix domain socket on that node. It handles node and workload attestation locally and relays to the server for signing. So in ambient the workload pod, ztunnel, and the agent ztunnel talks to are all on the same node: ztunnel reaches the agent over that node-local socket, and the agent talks to the central server on ztunnel's behalf.
Diagram: ztunnel fetches an SVID on a pod's behalf (ambient + SPIRE)
The pod carries nothing. ztunnel reads the workload's PID, asks the local SPIRE agent to attest it over the DelegatedIdentity API, and on success gets the pod's X.509-SVID to present on its behalf. The agent is a per-node DaemonSet exposing a host socket, all three boxes sit on the same node, and it relays to a central SPIRE server (one per trust domain) that signs the SVIDs. SPIRE adds multifactor attestation, node plus workload selectors, on top of the service account token the default mode trusts alone.
Where the cert stops carrying identity
This was the question that taught me the most, so I want to be precise about it. The certificate ztunnel presents is the X.509-SVID. The SPIFFE ID sitting in that certificate's URI SAN is exactly what makes it one. There is no "SPIFFE in the cert" on one side and a separate "SVID" on the other; they are the same object. For a direct ztunnel-to-ztunnel mTLS connection, that X.509-SVID is the entire mechanism, and it is enough. Each side presents its cert, each side verifies the other against the trust bundle, both ends are authenticated. Done.
It stops being enough the moment something terminates the connection. ztunnel is an L4 proxy: it does mTLS and the HBONE tunnel, and it carries the caller's identity across that L4 leg. But put an L7 proxy in the path, a waypoint, an ingress, a load balancer that ends the TLS and opens a fresh connection onward, and the certificate the next hop sees belongs to that proxy, not to the original caller. A peer certificate only ever proves the most recent hop. The original identity does not travel through an L7 termination on the strength of the cert alone.
Diagram: an L7 hop breaks the peer-cert chain
The peer cert only proves the last hop. A's X.509-SVID authenticates A to the proxy. The proxy then opens its own connection to B, so B sees the proxy's certificate. To carry A's original identity all the way to B you send it as a JWT-SVID, a token that rides in the request and survives the termination.
Beyond the SVID: where WIMSE fits
That L7 gap is not really a SPIFFE problem, it is the problem an IETF working group called WIMSE was chartered to standardise. WIMSE, which you say "wim-zee", stands for Workload Identity in Multi System Environments. The framing is simple: SPIFFE gives a workload a credential, but the ways teams actually carry and exchange that credential across HTTP and REST calls, and across trust boundaries, had grown up in relative isolation on top of OAuth, JWT and mTLS. WIMSE is the effort to harmonise those into one interoperable set of standards.
It generalises the vocabulary you have just learned. WIMSE talks about a trust domain, a workload identifier, and a workload identity credential, and it names the SPIFFE ID as an example identifier that already conforms. So nothing you set up for SPIFFE is wasted; WIMSE is the layer above it. The mapping is close to one to one:
| SPIFFE / SPIRE | WIMSE generalisation |
|---|---|
| SPIFFE ID | Workload identifier (the SPIFFE ID is a conforming example). |
| X.509-SVID | Workload Identity Certificate (X.509), used for mTLS. |
| JWT-SVID | Workload Identity Token (WIT): a JWT, but bound to a key, not a bearer token. |
| Trust bundle | Trust anchors for X.509, a JWK Set for tokens. |
| Direct mTLS only | An mTLS profile and an HTTP Message Signatures profile for L7 hops. |
The piece worth knowing is the Workload Identity Token, the WIT. It
is a JWT whose sub is the workload identifier and whose cnf
claim carries the workload's public key, so the token is bound to a key
rather than being a bearer credential. In other words a WIT is the close cousin of a
JWT-SVID: the same workload identity in a signed JWT, and its sub can
even be a SPIFFE ID, with one change that matters. The cnf key binding
makes it proof-of-possession instead of a bearer token, so a copied WIT is useless to
anyone who does not also hold the private key. Even on its own, as a verifiable,
non-replayable identity credential, that is useful.
How a workload then carries that identity is where WIMSE defines two profiles, and for most people the first is all they need. The mTLS profile uses the workload's certificate on the connection, which is exactly what a SPIFFE mesh already does: the X.509-SVID is presented in the handshake, the peer is authenticated, and nothing in the application changes. This is the practical path, and the one already running in production meshes today.
The second profile is for the case the earlier section raised, an L7 proxy that terminates TLS and breaks the peer-certificate chain. For that WIMSE defines an application-layer option that carries the WIT in the request and binds it with HTTP Message Signatures, so the original identity survives the hop without becoming a replayable bearer token. It is optional, it adds real complexity, and it is still maturing across the ecosystem, so read it as where the standard is heading for end-to-end L7 identity rather than something to reach for now. Start with mTLS.
Crossing trust domains is a separate problem again, where two SPIFFE roots cannot validate each other at all. There WIMSE leans on OAuth identity chaining and RFC 8693 token exchange, with one draft specifically about exchanging tokens at security boundaries. That is a different job from authenticating a single hop; it swaps one token for another at a boundary, and it is the same token-exchange pattern Solo already runs at the gateway today.
How to consume it today
A caveat before you go looking for something to install. WIMSE is still a set of
IETF Internet-Drafts, not a finished RFC, so there is no kind: WIMSE to
apply. What you can do today is build on the parts that are already real, and line
yourself up for the standard as it lands:
- Issue the credential with SPIFFE/SPIRE. A WIMSE workload identifier can be a SPIFFE ID, and an X.509-SVID already matches the Workload Identity Certificate shape. Everything in the first half of this post is the foundation, and none of it changes.
- Use OAuth token exchange (RFC 8693) for cross-boundary now. That is the exact mechanism WIMSE points at for crossing trust domains, and it is the token exchange at the gateway pattern you can run today.
- Lead with the mTLS profile, and track the rest. Carrying a WIT over mTLS lines up with what a SPIFFE mesh already does; the optional application-layer signature profile is the piece to follow as it and its reference implementations mature.
Naming, the part most people get wrong
SPIFFE IDs are names, and names outlive the reasons you chose them, so the spec spends real effort on what belongs in one. Two layers to get right: the trust domain and the path.
Trust domain
- Lowercase only, characters limited to
[a-z0-9.-_], no port and no userinfo, and at most 255 bytes. - There is no central registry handing out trust domain names, so global uniqueness is on you. The spec's advice is to suffix a domain you actually own, for example
prod.example.com, or for auto-generated environments to use a UUID. Two trust domains that collide cannot federate, because validation fails across distinct cryptographic roots. - Split trust domains along security boundaries, production versus staging, separate PKI or compliance zones, not for convenience. An "admin" assertion issued in domain A means nothing in domain B; trust domains are exactly the line across which you do not extend trust by default.
Path
- Segments use
[a-zA-Z0-9.-_], no empty segments, no.or.., no trailing slash. The trust domain is case-insensitive but the path is case-sensitive, and the whole URI must stay under 2048 bytes. - Map it to a stable platform identity. The Kubernetes convention,
/ns/<namespace>/sa/<serviceaccount>, is a good default because it tracks something the cluster already governs.
How this lands in Solo Enterprise for Istio
All of the above is the SPIFFE standard. Here is where you actually meet it in the
Solo stack. Out of the box, Istio already issues SPIFFE-format identities and uses
them for mTLS; the proxy's certificate is an X.509-SVID with a
spiffe://<trust-domain>/ns/<ns>/sa/<sa> SAN, attested
by the pod's service account token. You get SVIDs whether or not you have ever
heard the term.
Whether to step up from the istiod CA to SPIRE is a real decision, and the section below works through when it is worth it. The mechanics, once you have decided, are short.
In ambient mode the Solo distribution wires SPIRE in without touching your
workloads: you describe which pods get which SPIFFE ID with
ClusterSPIFFEID resources, and ztunnel does the fetching as the SPIRE
agent's delegate. The ID template is the same namespace-and-service-account shape
the naming section recommended.
apiVersion: spire.spiffe.io/v1alpha1 kind: ClusterSPIFFEID metadata: name: ambient-workloads spec: spiffeIDTemplate: "spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}" namespaceSelector: matchLabels: istio.io/dataplane-mode: ambient
One thing the YAML can hide: SPIRE is bring-your-own. It is the upstream CNCF project, so you deploy the server and the per-node agents yourself, with the SPIRE Operator or its Helm charts, then point Istio at them; Solo integrates with your existing SPIRE. The edition line matters too. Sidecar integration with SPIRE exists in upstream Istio through the SPIFFE CSI driver, while the ambient model shown here, with ztunnel fetching SVIDs as the agent's delegate so nothing changes in your pods, is a Solo Enterprise for Istio capability.
Two integration patterns are worth knowing about. The first is the one above: SPIRE as the identity source for ambient workloads, with ztunnel as delegate. The second roots the whole mesh in SPIRE: you can have istiod itself use SPIRE-issued X.509-SVIDs as its CA material instead of its default self-signed CA, by running a helper alongside istiod that continuously fetches and rotates SVIDs from the local SPIRE agent and pointing istiod at them with its custom certificate provider mode. The result is a single chain, from the control plane down to every proxy, rooted in your SPIRE trust domain, which is usually what the PKI and compliance teams are actually asking for.
When to actually run a SPIRE server
SPIRE is a deliberate step up, not a default-on upgrade, so it is worth being clear about when it earns its place. The whole question is whether a valid Kubernetes service account token is a strong enough basis for identity. Three situations say no.
- You need stronger attestation. The default issues an SVID to anything presenting a valid service account token, a single factor that can be exfiltrated or over-mounted. SPIRE issues an SVID only after multifactor attestation: node attestation that the node itself is genuine (cloud instance identity, a TPM, the Kubernetes projected token), plus workload selectors that pin the service account, namespace and container image. An attacker cannot mint
spiffe://…/sa/paymentsjust by holding a token; the node and the workload have to match a registration entry too. - Identity has to reach beyond the mesh. The Istio CA only serves the mesh. Once identity must cover VMs, several clusters or clouds, or services that are not in Istio at all, you want a single authority across all of it, with federation between trust domains. That is what SPIRE is; the Istio CA is not.
- Compliance or PKI requires it. Certificates rooted in your own PKI, a single audited chain from control plane to workload, centralised issuance and rotation under policy. This is the istiod-CA-rooted-in-SPIRE pattern, and it is usually what the PKI and audit teams are really asking for.
If you want to see the verification side of the same identity story, the JWKS post covers how a gateway checks the JWTs an IdP signs, which is the bearer-token cousin of the JWT-SVID, and the Zero Trust for AI agents post puts SPIFFE identity and mTLS in the wider context of where Solo's controls sit. For on-behalf-of token exchange, where one identity acts for another across a hop, the token exchange lab is the hands-on version.
Sources and further reading
- SPIFFE concepts, spiffe.io. Definitions of the SPIFFE ID, SVID, X.509-SVID, JWT-SVID, trust domain and the Workload API, including the no-secret bootstrap and automatic key rotation.
- The SPIFFE ID standard, spiffe/spiffe. The URI format, the trust domain and path constraints, the length limits, and the Security Considerations on why volatile authorization attributes do not belong in the ID.
- SPIRE Workload Attestation for ambient mesh, Solo documentation, docs.solo.io. ztunnel as a delegate over the SPIRE DelegatedIdentity API, and the
ClusterSPIFFEIDtemplates for ztunnel, waypoints and workloads. - Using SPIFFE/SPIRE with the Istio control plane CA, Solo documentation, docs.solo.io. Rooting istiod's CA material in SPIRE-issued SVIDs via a helper and the custom certificate provider.
- WIMSE working group, IETF, datatracker.ietf.org/wg/wimse. Charter and scope: harmonising OAuth, JWT and SPIFFE for workload identity across multi-system environments, including the SPIFFE-to-OAuth claim mapping milestone.
- WIMSE Architecture, draft-ietf-wimse-arch. Trust domain, workload identifier and workload identity credential; the Workload Identity Certificate and Workload Identity Token forms; provisioning and proof-of-possession.
- Workload-to-Workload Authentication with HTTP Message Signatures, draft-ietf-wimse-http-signature. The
Workload-Identity-Tokenheader, the WIT with itscnf.jwkkey binding, and the RFC 9421 signature that proves possession across L7 hops.