SPIFFE Federation Done Safely: JWT-SVIDs Across Trust Domains
Federate SPIRE trust domains without opening a confused-deputy hole — bind the JWT-SVID audience, validate the federated bundle, and know when X.509 mTLS is the safer choice.
- #security
- #ai
- #spiffe
- #spire
- #workload-identity
Inside a single SPIFFE trust domain, workload identity is clean: every workload gets a verifiable SPIFFE ID, and mutual TLS proves who is who. The moment you federate two trust domains — two clusters, two clouds, two teams’ SPIRE deployments — that clean local guarantee becomes a distributed trust problem, and the failure modes get sharp. The most common one is a confused-deputy hole created by a single missing check on a JWT-SVID’s audience claim, which lets a token minted for one service be replayed against another across the federation. Federation is a powerful pattern, but it extends your trust boundary into someone else’s control, and that demands more care than the single-domain case ever did.
Federation Means Trusting Another Domain’s Issuer
SPIRE federation works by exchanging trust bundles — the set of public keys each domain uses to sign its workloads’ SVIDs. When domain A federates with domain B, A obtains B’s bundle (typically via B’s federation bundle endpoint) so that A can verify SVIDs presented by B’s workloads. That is the whole mechanism, and it is also the whole risk: A is now extending trust to whatever B’s SPIRE server signs.
# SPIRE server federation: trust domain B's bundle, fetched from its endpoint
federation {
bundle_endpoint { address = "0.0.0.0" port = 8443 }
federates_with "domain-b.example" {
bundle_endpoint_url = "https://spire.domain-b.example:8443"
bundle_endpoint_profile "https_spiffe" {
endpoint_spiffe_id = "spiffe://domain-b.example/spire/server"
}
}
}
The https_spiffe profile here matters: it authenticates the bundle endpoint using SPIFFE itself, so A knows it is fetching B’s bundle from B’s real server, not from an impostor. Using the weaker https_web profile means trusting whatever web PKI says about that endpoint, which is a wider attack surface. A rogue bundle injected here would let an attacker mint SVIDs your domain trusts — so how the bundle is fetched is a security decision, not a config detail.
The Audience Bug That Breaks Everything
JWT-SVIDs are bearer tokens. Whoever holds a valid one can present it, and unlike X.509-SVID mTLS, there is no proof of possession of a private key bound to the channel. The defense against replay is the aud (audience) claim: a JWT-SVID is minted for a specific audience, and the relying party must reject any token whose audience is not itself.
This is the check teams forget. If service B in domain B accepts a JWT-SVID without verifying that aud matches B’s own identifier, then a token a workload obtained for service A can be presented to B and accepted. That is a classic confused deputy across the federation boundary — the token was legitimate, just not for B. Worse is the relying party that accepts any audience or a wildcard, which removes the check entirely.
# Correct: bind the audience to THIS service, reject anything else
expected_audience = "spiffe://domain-b.example/service-b"
claims = jwt_svid.parse_and_validate(
token,
audience=expected_audience, # rejects tokens minted for a different aud
trust_bundle=federated_bundle_b, # validates against the FEDERATED bundle
)
# Then check the SPIFFE ID's trust domain matches the expected peer:
assert claims.spiffe_id.trust_domain == "domain-a.example"
Two checks, both mandatory: the audience must be this service, and the SVID must validate against the federated bundle for the correct domain — not a blanket trust of any signer your server happens to know. Skip either and federation becomes a trust hole.
When X.509-SVID mTLS Is the Better Tool
Because JWT-SVIDs are replayable bearer tokens until they expire, they are the wrong default for service-to-service calls inside a mesh, where X.509-SVID mTLS gives you proof of possession and channel binding for free. Reserve JWT-SVIDs for the cases that genuinely need a portable token — crossing a boundary where you cannot do mTLS, or authenticating to something that consumes a JWT — and keep TTLs short so a leaked token’s window is small.
This is the same trade-off that shows up in mTLS and service identity design: the strongest identity proof is a key the holder must possess and use in the handshake, not a token they can copy. If your federation is doing pure service-to-service traffic, prefer X.509-SVIDs over JWT-SVIDs and you remove the entire replay class of bug.
Asymmetric Trust Is Usually Correct
A subtle design point: federation does not have to be bidirectional, and often should not be. If domain A’s workloads need to call into domain B, A needs B’s bundle to verify B’s SVIDs — but B does not necessarily need to trust A. Teams frequently set up bidirectional federation reflexively and end up extending trust they never needed to grant. Map the trust graph as a directed structure and grant only the direction each integration requires.
Pro Tip: Draw the trust direction as arrows before you write any federation config. “A trusts B” and “B trusts A” are two separate decisions with two separate blast radii. Every arrow you draw is a domain whose compromise can now mint identities yours will accept — so the smallest correct set of arrows is the goal, not symmetry for its own sake.
Using AI to Audit the Validation Path
The audience and trust-domain checks are easy to get subtly wrong, and the bug is invisible until exploited. A model is useful for auditing the relying-party validation against the spec — as a reviewer that flags missing checks, with you confirming against the real code:
Prompt: “Review this JWT-SVID validation for a cross-trust-domain SPIFFE federation. Confirm whether it binds the aud claim to this service and rejects other audiences, and whether it validates against the correct federated bundle and expected peer trust domain. Flag any place that accepts any audience, a wildcard, or any signer. Treat a missing check as a finding, don’t assume it’s done elsewhere.”
Output (excerpt): “FINDING (high): the validation passes
audience=None, which accepts a JWT-SVID minted for ANY audience — a confused-deputy hole. A token a peer obtained for service-a can be replayed here. Fix: pass this service’s own SPIFFE ID as the required audience. FINDING (medium): the trust bundle used is the server’s full known-bundle set, not the specific federated bundle for domain-a; this trusts any signer the server knows, not just the intended peer. Pin to the federated bundle and assert the SVID’s trust_domain equals the expected peer.”
The model is good at catching the missing aud binding and the over-broad bundle trust precisely because those are easy for a human to skim past. But you confirm the findings against the actual code and own the fix — the AI flags, you verify. If you want a full review of your federation topology and validation path, the SPIFFE JWT-SVID federation review prompt maps the trust graph, hunts the audience bug, and produces a severity-ordered findings list with corrected config.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.