Most SPIFFE deployments issue both credential formats, and the security of the estate depends on which one each call path actually uses. An X.509-SVID proves identity with a private key inside a TLS handshake and cannot be replayed without that key, but the proof ends wherever a proxy terminates TLS. A JWT-SVID survives load balancers, gateways and queues, but it is a bearer token: whoever holds it can use it until it expires, against every audience it names. This page goes beyond the format overview in the SPIFFE and SPIRE Guide and focuses on the decision itself: replay and audience risk, what happens at Layer 7 proxies, rotation and bundle behaviour, and how to choose per call path. It is for platform engineers, mesh owners and identity architects running a SPIFFE trust domain. See the SPIFFE and SPIRE Guide for SPIRE architecture and attestation.
Key takeaways
- Default to X.509-SVIDs. Use them wherever both ends, or the sidecars in front of them, can complete mTLS with each other, because a stolen certificate is useless without its private key.
- Use JWT-SVIDs only where TLS breaks. The JWT-SVID specification describes them as aimed at asserting identity across Layer 7 boundaries, so reserve them for hops through TLS-terminating intermediaries, asynchronous paths and external relying parties.
- The audience is the blast radius. A JWT-SVID minted with several audiences can be replayed by any one of those receivers to the others, so mint one audience per target service and reject everything else.
- Proxies change who you are authenticating. After TLS termination the backend sees the proxy, not the caller, unless identity is carried forward in a token or a trusted header that the proxy sanitises.
- Proof of possession is the long-term fix. The incubating WIT-SVID format and the IETF WIMSE drafts bind a token to a workload key, removing pure bearer replay, but they are still drafts and should be piloted, not depended on.
The short answer
Choose the X.509-SVID for any synchronous call where the caller's TLS session reaches the callee or the callee's sidecar unbroken. Choose the JWT-SVID when the request has to pass a component that terminates TLS and re-originates it (an external load balancer, an API gateway, a CDN, a managed ingress), when there is no live connection at all (message queues, event buses, stored jobs), or when the relying party is a system that understands JWTs but not your trust bundle, such as a cloud provider or OAuth service accepting federated tokens. When you need both properties, run mTLS on every hop you control and carry a single-audience, short-lived JWT-SVID inside it, as the SPIRE Envoy JWT tutorial does.
Side-by-side comparison
| Decision factor | X.509-SVID | JWT-SVID |
|---|---|---|
| What an attacker must steal | Certificate plus private key (T1552.004) | The token string only (T1528, then T1550.001) |
| Replay window | None without the key | Until exp, at every audience listed |
| Who can replay it | Nobody who lacks the key | Any interceptor, log reader, or receiver named in aud |
| Survives TLS termination | No: the proof ends at the terminating hop | Yes: travels in the Authorization header end to end |
| Works without a live connection | No | Yes (queues, batch, stored requests) |
| Scoping mechanism | Peer SPIFFE ID checked by the server and client | aud claim, checked by the validator |
| Validation material | CA certificates in the bundle (use x509-svid) | JWKs in the bundle (use jwt-svid, kid required) |
| SPIRE default lifetime | 1 hour (default_x509_svid_ttl) | 5 minutes (default_jwt_svid_ttl) |
| Encrypts traffic | Yes, as part of mTLS | No: specification requires confidentiality on every hop |
| Typical failure | Permissive mTLS, proxy forwarding unauthenticated identity headers | Broad or unchecked audience, token leakage into logs, long TTL overrides |
The SPIFFE Workload API specification makes both profiles mandatory for implementations, while allowing operators to disable either one in a deployment. SPIRE exposes this as disable_jwt_svids in the server configuration. If no call path in a trust domain needs bearer tokens, turning the JWT profile off removes a whole class of replay risk.
X.509-SVIDs: threat model, architecture and failure modes
Architecture in words
The workload, or a sidecar acting for it, fetches its certificate, private key and trust bundle from the local Workload API and streams updates before expiry. On each connection, both peers present certificates, prove possession of the key in the handshake, validate the chain against the bundle for the peer's trust domain, and then apply authorisation policy to the peer's SPIFFE ID, taken from the single URI SAN. The SPIFFE X.509-SVID specification requires validators to reject a leaf with more than one URI SAN, a cA flag of true, or the keyCertSign or cRLSign key usages.
Threat model
- Key theft from the node: an attacker with root on a node can read keys held in memory by local workloads. The exposure is bounded by the certificate lifetime and by which identities the node's agent was authorised to hold.
- Mis-issuance: loose registration selectors hand a valid certificate to the wrong process.
- Termination points: any component that terminates TLS becomes the authenticated peer. The backend learns nothing cryptographic about the original caller.
Failure modes
- Permissive or opportunistic mTLS: meshes left in a mode that accepts plaintext as well as mTLS give the appearance of workload identity while accepting unauthenticated calls.
- Trusting a forwarded certificate header: Envoy's x-forwarded-client-cert (XFCC) header carries the hash and URI SAN of client certificates that a request passed through. Envoy sanitises it by default when forward_client_cert_details is unset. Set it to forward or append, and a backend reachable by other paths can be fed a forged header, so only accept XFCC on connections that are themselves mTLS from a known proxy SPIFFE ID.
- Authorising on trust domain only: accepting any certificate from the bundle, instead of an allow list of SPIFFE IDs, turns one compromised workload into access everywhere.
Controls
Enforce strict mTLS on every mesh namespace that carries production traffic, write authorisation policy against exact SPIFFE IDs, keep certificate lifetimes at or below the SPIRE default of one hour for workloads, and treat any proxy that terminates TLS as a workload with its own SPIFFE ID and its own policy. See the Service Mesh Security Guide for mesh modes and policy, and the mTLS Guide for handshake detail.
JWT-SVIDs: threat model, architecture and failure modes
Architecture in words
The caller asks the Workload API for a JWT-SVID with FetchJWTSVID, which requires the audience it intends to authenticate against. It sends the token in the HTTP Authorization header with the Bearer scheme, or in the authorization metadata key for gRPC, as the specification recommends. The receiver either validates locally, using JWKs whose use parameter is jwt-svid, or hands the token to its own SPIRE agent through ValidateJWTSVID with its own audience value. The specification limits the header to alg, kid and typ, allows only RS, ES and PS signatures at 256, 384 or 512 bits, and forbids JWS JSON serialisation.
Threat model
- Bearer replay: the specification states plainly that JWT-SVIDs are susceptible to replay and that requiring aud and exp improves but does not solve the problem. It recommends an aggressive exp value.
- Audience pivot: the specification's own example: if Alice holds a token for Bob and Chuck and sends it to Chuck, Chuck can impersonate Alice to Bob.
- Exposure at intermediaries: every proxy that terminates TLS sees the token in cleartext. The specification requires confidentiality on every hop, from workload to load balancer and from load balancer to the next workload, with only non-network links such as a local Unix domain socket excepted.
- Leakage through observability: access logs, tracing spans and error reports that capture the Authorization header turn a five-minute credential into one that anyone with log access can use within its window.
Failure modes
- Shared or generic audiences: minting tokens for an audience such as the trust domain name or a value like prod means every service in production accepts every token.
- Gateway filters that skip audience checks: Envoy's JWT authentication filter documentation states that if audiences is not specified, the audiences in the JWT are not checked. A gateway configured with only a JWKS source accepts a token minted for any other service.
- Forwarding the original token deep into the estate: a gateway that validates a JWT-SVID and then forwards it (the filter's forward option) lets every downstream service, and every log on the way, hold a replayable copy.
- TTL overrides: per-entry or per-request TTL increases made for a batch job that "kept failing" leave hour-long bearer tokens in circulation.
- Relying on jti: the specification permits jti but says validators are not required to track its uniqueness. Unless every validator keeps a replay cache, jti is a label, not a control.
Controls
One audience per token, set to the receiving service's SPIFFE ID (for example spiffe://prod.example.com/billing/api); validators configured with an explicit expected audience; the SPIRE default five-minute lifetime or shorter; Authorization header redaction in every proxy and logging pipeline; and a narrower internally issued token at the edge instead of forwarding inbound tokens. See the JWT Security Guide for validation rules and the JWT and Session Tokens in Code and Logs Guide for log hygiene.
What actually happens at Layer 7 proxies
The proxy decides what the backend can still verify. Four topologies cover most estates.
| Topology | What the backend can verify | Recommended pattern | Residual risk |
|---|---|---|---|
| Sidecar to sidecar inside one mesh | Caller's X.509-SVID, end to end between proxies | Strict mTLS, policy on exact SPIFFE IDs; no JWT needed | Pod-local hop between app and sidecar; node compromise |
| Edge load balancer or gateway terminates TLS, then mTLS to backend | The gateway's X.509-SVID only | Caller sends a single-audience JWT-SVID; gateway validates and re-mints or passes on; gateway to backend over mTLS | Gateway sees all tokens in cleartext; gateway is a high-value workload |
| Proxy terminates TLS and forwards certificate details in a header | Whatever the header claims | Only if the backend accepts the header solely over mTLS from the proxy's SPIFFE ID and the proxy sanitises inbound copies | Header forgery from any path that bypasses the proxy |
| Queue, event bus or scheduled job | Nothing at transport level between producer and consumer | JWT-SVID with audience set to the consumer, short TTL, or sign the message itself | Queued messages outliving token expiry; tokens stored at rest in the broker |
The pattern in the SPIRE Envoy JWT tutorial is worth copying as a reference design: frontends and backend talk over Envoy mTLS using X.509-SVIDs, and an external authorisation filter on each side obtains a JWT-SVID from the local SPIRE agent with a configured audience and validates it on the receiving side.
RFC 8705 certificate-bound tokens do not escape the proxy problem: the RFC says how a TLS-terminating intermediary passes the client certificate to the application is out of scope. If you bind tokens to certificates, you still need a trusted forwarding channel. See mTLS vs Signed Tokens for Service Authentication for the wider binding options, including DPoP.
Rotation, bundles and revocation
- Lifetimes: SPIRE's documented defaults are one hour for X.509-SVIDs, five minutes for JWT-SVIDs and 24 hours for the CA signing key (ca_ttl). The asymmetry is deliberate: a bearer token needs a far shorter life than a key-bound certificate to carry similar risk.
- Revocation in practice: neither format relies on CRLs or token introspection in normal SPIFFE use. The control is expiry, so the lifetime you set is your worst-case revocation delay for a stolen credential.
- Bundle refresh: the trust bundle specification's spiffe_refresh_hint tells consumers how often to re-fetch. The specification notes this governs how quickly a compromised key can be redacted, and suggests a low default of about five minutes where no hint is set. Validators that cache JWKS for hours will keep accepting tokens signed by a key you have already withdrawn.
- Separate key types: SPIRE lets you set jwt_key_type independently of ca_key_type (default ec-p256), and set jwt_issuer for relying parties that require an iss claim. Configure jwt_issuer only when an external system needs it, because an OIDC-style issuer invites federation you may not have reviewed.
Where proof of possession is heading
The SPIFFE project has added a third format, the WIT-SVID, marked Incubating in the specification repository. It is a sub-profile of the IETF WIMSE Workload Identity Token: a JWT that carries the workload's public key in a cnf claim. The WIT-SVID specification forbids an aud claim in the token itself, requires validators to reject it without a proof of possession of the bound key, and says jti must not be used for replay protection, because the same token may be issued to several instances on one node. Scoping to a recipient moves into the proof.
As checked on 14 November 2026, draft-ietf-wimse-workload-creds is at version 03 (30 October 2026), an Internet-Draft with the datatracker state "I-D Exists". It describes the token as not a bearer token, sent in a Workload-Identity-Token header with a proof such as a Workload Proof Token or HTTP Message Signatures, and asks that the proof be time-limited, scoped to the target and replay-protected. It also notes that intermediaries can still read, capture and replay the token and proof, so deployments must decide which intermediaries to trust. Drafts change: pilot WIT-SVIDs on one internal path only. See the WIMSE Workload Identity Architecture Guide.
Decision criteria
| If the call path... | Choose | Configure |
|---|---|---|
| Runs between two workloads or sidecars in the same mesh | X.509-SVID | Strict mTLS, SPIFFE ID allow list |
| Crosses a managed load balancer or gateway you cannot give an SVID | JWT-SVID | aud = backend SPIFFE ID, TTL 5 minutes or less, TLS on both legs |
| Crosses a gateway that holds its own SVID | Both | mTLS to gateway and backend, JWT-SVID for the original caller, gateway checks aud |
| Goes through a queue or event bus | JWT-SVID or signed message | aud = consumer, TTL covering queue latency only, no storage of tokens beyond expiry |
| Federates to a cloud or OAuth relying party | JWT-SVID | aud exactly as the relying party requires, jwt_issuer only if demanded, exchange immediately for a scoped cloud credential |
| Needs end-user context | Neither alone | Transport SVID plus a user or transaction token; see the OAuth Transaction Tokens (Txn-Tokens) Guide |
| Has no requirement for bearer tokens anywhere in the trust domain | X.509-SVID only | disable_jwt_svids on the SPIRE server |
Small platforms: start X.509 only, add the JWT profile when the first TLS-terminating hop appears, and keep one owner for audience naming. Large estates: publish an audience naming standard (audience equals receiving SPIFFE ID), a list of approved validators, and a register of every path that uses JWT-SVIDs with its reason, reviewed like firewall exceptions.
Worked scenario: a payments API behind a managed load balancer
A payments platform runs in Kubernetes with a mesh using X.509-SVIDs. A partner-facing reporting service in another cluster calls the payments API through a cloud managed load balancer that terminates TLS and cannot hold an SVID. The first design trusted XFCC from an ingress proxy and minted JWT-SVIDs with the audience prod.
- Map the path: reporting workload to load balancer (TLS), load balancer to ingress proxy (TLS), ingress to payments API (mesh mTLS). The payments API's verifiable peer is the ingress proxy, so its X.509 check proves nothing about the reporting service.
- Remove the forged-header route: the ingress is set to sanitise XFCC, and the payments policy stops reading it, because the load balancer cannot present client certificate details in a way the ingress can verify.
- Fix the audience: the reporting service now requests a JWT-SVID with aud spiffe://prod.example.com/payments/api. The ingress JWT filter lists exactly that audience; the payments service also validates with ValidateJWTSVID using its own audience.
- Shorten and contain: TTL stays at the five-minute default; the reporting service fetches per call batch rather than caching for its own process lifetime; access logs drop the Authorization header at the load balancer and ingress.
- Authorise twice: the payments service authorises the transport peer (only the ingress SPIFFE ID may connect) and the token subject (only the reporting SPIFFE ID may call the reporting endpoints).
- Test and roll back: run the ingress filter in a mode that logs audience mismatches for a week, then enforce. Rollback is reverting the filter configuration; the mesh mTLS layer remains unchanged throughout.
Outcome: a leaked token is valid only against the payments API, for at most five minutes.
Detection and measurement
- Log: SPIRE agent JWT-SVID issuance per selector set and audience; validator rejections by reason (audience, expiry, signature, algorithm); mesh connections accepted in plaintext or permissive mode; any request at a backend carrying XFCC from a peer that is not an approved proxy.
- Alert on: one subject's JWT-SVID presented from network locations or pods that do not run that workload; tokens arriving with more than one audience; validation successes for tokens close to the end of an unusually long lifetime.
- Rate limits: the SPIRE agent configuration includes per-selector rate limits for FetchJWTSVID, disabled by default. Enabling a sensible ceiling bounds a compromised workload's ability to mint tokens for many audiences.
Metrics that show the design is working:
- Share of service-to-service call paths authenticated by X.509-SVID alone (target: every path without a terminating hop).
- Number of JWT-SVID audiences with more than one value in issued tokens (target: zero).
- Validators configured without an explicit expected audience (target: zero).
- Maximum JWT-SVID lifetime issued in the last 30 days against the five-minute default.
- Percentage of proxies and log pipelines that redact the Authorization header.
Common failure patterns
- The "JWT everywhere" shortcut: teams use JWT-SVIDs inside the mesh because one library made it easy, giving up key binding on paths that never needed bearer tokens.
- Trusting identity headers from anywhere: backends that read XFCC or an injected identity header without checking that the connection came from the proxy.
- Audience equals environment: one audience such as prod reused across services, so every receiver can impersonate callers to every other.
- Stale JWKS caches: validators holding keys for longer than the bundle refresh hint, so a redacted key keeps working.
Framework mapping
| Framework | Reference | How the SVID choice applies |
|---|---|---|
| NIST SP 800-53 Rev. 5 | IA-9 Service Identification and Authentication | Both formats authenticate services; record which format each path uses and why |
| NIST SP 800-53 Rev. 5 | IA-5(2) Public Key-based Authentication; SC-17 Public Key Infrastructure Certificates | X.509-SVID issuance, validation and bundle management |
| NIST SP 800-53 Rev. 5 | SC-8 Transmission Confidentiality and Integrity; SC-23 Session Authenticity | Confidentiality on every hop carrying JWT-SVIDs; mTLS session binding |
| NIST SP 800-53 Rev. 5 | SC-12 Cryptographic Key Establishment and Management | CA and JWT signing key types, lifetimes and rotation |
| NIST CSF 2.0 | PR.AA-03; PR.DS-02 | Services are authenticated; data in transit is protected |
| MITRE ATT&CK | T1528 Steal Application Access Token; T1550.001 Application Access Token | Theft and replay of JWT-SVIDs |
| MITRE ATT&CK | T1552.004 Private Keys; T1557 Adversary-in-the-Middle | Key theft for X.509-SVIDs; interception that mTLS resists |
Practitioner checklist
- Inventory every call path and record whether it has a TLS-terminating hop.
- Use X.509-SVIDs with strict mTLS and SPIFFE ID allow lists on every path without one.
- If no path needs bearer tokens, set disable_jwt_svids on the SPIRE server.
- Mint JWT-SVIDs with exactly one audience, equal to the receiving service's SPIFFE ID.
- Configure an explicit expected audience in every validator, gateway filter and ext_authz service.
- Keep JWT-SVID lifetimes at five minutes or less and review every TTL override.
- Redact the Authorization header in proxy access logs, traces and error reporting.
- Accept forwarded certificate headers only over mTLS from a named proxy identity; sanitise them at the edge.
- Align validator JWKS caching with the bundle refresh hint.
Standards and references
- SPIFFE: The JWT SPIFFE Verifiable Identity Document
- SPIFFE: The X.509 SPIFFE Verifiable Identity Document
- SPIFFE: SPIFFE Workload API
- SPIFFE: SPIFFE Trust Domain and Bundle
- SPIFFE: The WIT SPIFFE Verifiable Identity Document
- IETF: WIMSE Workload Credentials (Internet-Draft)
- IETF: RFC 8705 OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
- SPIRE: Server full configuration reference
Related NHI Mgmt Group resources: SPIFFE and SPIRE Guide, SPIFFE vs Cloud-Native Workload Identity, mTLS vs Signed Tokens for Service Authentication, DPoP Guide, Cloud Workload Identity Guide.