Most infrastructure authentication today is possession-based: whoever holds the API key, the database password, the token gets treated as authorized, whether or not they’re actually the workload that credential was ever meant for. A stolen secret and a legitimate one look identical to whatever’s checking, because nothing was ever asked to prove what it is, only that it’s holding the right string. SPIFFE exists to replace that with attested identity: a cryptographic, short-lived identity for a workload, an SVID, scoped to a trust domain, verifiable by anyone who trusts that domain’s root, derived from what the workload actually is rather than what it happens to be carrying, and independent of IP address, hostname, or anything else about the network it’s running on. It’s a clean answer to a real problem, which is why it shows up in identity roadmaps at companies of every size.
What’s less consistent is what happens after a team decides to adopt it.
What the reference implementation actually requires
SPIFFE is a specification. It describes the identity model, not how to run it. SPIRE, the CNCF reference implementation, is what most teams reach for to make that model real, and installing and maintaining it is no small feat. It means assembling several separate projects into one working system:
- A SPIRE Server, the source of trust for a domain, plus a SPIRE Agent running on every node that needs to issue identities.
- On Kubernetes, a controller manager to keep registration entries, the mapping from “this is what a workload looks like” to “this is the SPIFFE ID it gets”, in sync with what’s actually running in the cluster. Off Kubernetes, that same mapping has to be maintained some other way, usually by hand.
- A policy layer, usually a service mesh like Envoy or Istio, or a separate engine like OPA, since SPIRE issues identity but has no opinion on what a given identity is allowed to do.
- A management UI, commonly Tornjak, for anyone who wants visibility into registrations and trust bundles without reading server logs.
None of this comes pre-integrated. Every one of those components is a separate release cadence, a separate set of upgrade notes, and a separate way for the chain to break. Getting from “we picked SPIFFE” to “every workload we care about has a real, enforced identity” tends to run longer than the initial estimate, and the gap between those two dates is rarely a SPIRE bug. It’s the assembly work nobody scoped up front.
None of that is a knock on SPIRE’s design. It was built to prove the specification works in production, and it succeeded at that. It was not built to be the finished security platform a platform team runs unattended for years. Certificate issuance and rotation are SPIRE’s job. What a workload is allowed to do with that identity, and what happens when the workload isn’t a clean fit for SPIRE’s model in the first place, is left to whoever owns the rollout.
Standing up the ecosystem is only half of it
Everything above is the operational cost of running SPIRE: infrastructure you install once and then keep alive. There’s a second cost that’s easy to undercount, because it isn’t something you install once. It’s a requirement placed on every individual workload, for as long as that workload exists.
SPIRE’s delivery model is pull-based. A workload gets an SVID by calling the SPIRE Agent’s Workload API itself, over a Unix domain socket, using a client library, then loading the returned certificate and private key into its own process memory, then handling the TLS handshake, then calling the API again before the certificate expires. None of that happens unless the workload’s own code does it. A CSI driver or a SPIFFE-Helper sidecar can do this on a workload’s behalf if the workload itself can’t, but that’s a workaround for the requirement, not a removal of it: something still has to actively participate in the exchange.
For a service you’re actively building, that participation is a library import and some glue code, real work, but bounded, and something a team can plan for. For anything you’re not actively rewriting, a legacy application, a third-party binary, a vendored dependency, an off-the-shelf appliance, that path isn’t available at all. The only option left is a sidecar or proxy that terminates TLS in front of it, and that trade solves the participation problem by creating a different one: the certificate now belongs to the proxy process, not the workload it’s supposed to represent, and every workload that can’t speak SPIFFE natively needs its own companion process kept alive indefinitely.
This is why “we deployed SPIRE” and “every workload we care about has a real identity” are different sentences, and why the second one tends to take longer than the first. Standing up the server, the agents, and the policy layer is an infrastructure project with a schedule. Getting every workload to actually participate, or fronting the ones that can’t, means touching or wrapping every single thing that’s supposed to have an identity, and that list rarely shrinks on its own.
Where the gaps show up in practice
A few places this tends to bite, regardless of how well the initial rollout goes:
Getting to day one. Before a workload can have an identity at all, SPIRE forces a choice: rewrite it to call the Workload API directly, or stand up a sidecar in front of it. There’s no third option. Day-one coverage across a real fleet is capped by how much of it fits cleanly into one of those two buckets, and everything that doesn’t yet, the service waiting on a rewrite, the appliance waiting on a sidecar nobody’s built, runs with no identity at all in the meantime.
Policy, as someone else’s project. SPIRE issues an identity and stops there. What that identity is actually allowed to do gets decided somewhere else entirely, a service mesh, an OPA install, a set of hand-written admission rules, with its own configuration language, its own release cadence, and its own way of drifting out of sync with what SPIRE issued. Two systems agreeing on who a workload is isn’t the same as them agreeing on what it can do, and keeping both in step, as either one changes, is a permanent job, not a rollout task.
CI/CD. Ephemeral runners spin up, do one job, and disappear, which is exactly the case SPIRE’s registration model assumes doesn’t happen often. Even a runner whose code is perfectly happy to call the Workload API still needs to be registered and attested in time to be useful, and getting that timing right for short-lived GitHub Actions or similar runners takes custom tooling most teams have to build themselves.
Every one of these is solvable. None of them is solved by SPIRE alone, and the tooling that closes each gap is one more thing to keep patched, monitored, and correctly wired into everything else.
A different way to close the gap
We build Riptides on the same specification. We just made a different architectural bet: instead of adding a component for each gap above, and instead of asking every workload to participate in its own identity delivery, we moved identity issuance and policy enforcement into the layer every workload already passes through no matter its language, its framework, or where it’s deployed, which is the Linux kernel.
Riptides issues SPIFFE-compliant X.509 SVIDs directly inside the kernel, bound to the real process making the connection. There’s no Workload API for an application to call and no certificate for it to load, because the kernel attests the process and injects the identity into the connection itself. Policy is checked and enforced at the same point, before a connection is established, rather than handed off to a mesh or an external engine. We’ve written separately about why this matters specifically for AI agents; this post is about the plainer case, two ordinary workloads running as unmodified pods in a vanilla Kubernetes cluster.
Rather than argue that in the abstract, here’s what it looks like end to end.
What this looks like
Picture a small, otherwise-vanilla Kubernetes cluster, no Istio, no Linkerd, no mesh already in place, so the comparison holds against what a from-scratch SPIRE rollout would need to add, not against infrastructure that already solved half the problem.
orders-api, a Deployment and Service running a tiny HTTP server on port 8080 that returns a canned JSON response:
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: orders-api
template:
metadata:
labels:
app.kubernetes.io/component: orders-api
spec:
containers:
- name: orders-api
image: python:3.12-slim
command: ["python3", "-c"]
args:
- |
import http.server, json
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"order_id": 42, "status": "packed"}).encode())
http.server.HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: orders-api
namespace: demo
spec:
selector:
app.kubernetes.io/component: orders-api
ports:
- port: 8080
targetPort: 8080
checkout, a Deployment that polls it every couple of seconds:
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: checkout
template:
metadata:
labels:
app.kubernetes.io/component: checkout
spec:
containers:
- name: checkout
image: curlimages/curl:latest
command: ["sh", "-c"]
args:
- "while true; do curl -s http://orders-api.demo.svc.cluster.local:8080/orders; echo; sleep 2; done"
Neither manifest mentions Riptides, SPIFFE, or TLS anywhere. That’s deliberate: this is what the two workloads look like with no identity story at all, and it’s exactly what stays in place afterward.
Assumes: a Riptides control plane and a daemon already running on the cluster. The daemon install is a single Helm chart, one pod per node, no CSI driver, no admission webhook, no separate controller manager to reconcile against cluster state. See Getting Started for the install itself. Installing it doesn’t change anything on its own, there’s no WorkloadIdentity yet for it to enforce.
Giving both workloads a verified identity and requiring mTLS between them is two Riptides resources, following the same backend/frontend pattern as the docs’ own mTLS between services guide: a WorkloadIdentity for the server requiring mTLS from a specific caller, and a WorkloadIdentity for the client.
Not to be confused with these being Kubernetes resources in name only: the Riptides control plane speaks the native Kubernetes API, so the same GitOps and Kubernetes management tooling already in use can manage them directly.
apiVersion: core.riptides.io/v1alpha1
kind: WorkloadIdentity
metadata:
name: demo-orders-api
spec:
workloadID: demo/orders-api
scope:
daemonGroup:
id: riptides/daemongroup/demo-cluster
selectors:
- k8s:container:name: orders-api
k8s:label:app.kubernetes.io/component: orders-api
process:name: python3
connection:
tls:
mode: MUTUAL
allowedSPIFFEIDs:
inbound:
- spiffe://acme.corp/demo/checkout
---
apiVersion: core.riptides.io/v1alpha1
kind: WorkloadIdentity
metadata:
name: demo-checkout
spec:
workloadID: demo/checkout
scope:
daemonGroup:
id: riptides/daemongroup/demo-cluster
selectors:
- k8s:container:name: checkout
k8s:label:app.kubernetes.io/component: checkout
riptides-cli ctl apply -f workload-identities.yaml
Neither the orders-api container nor the checkout container needed a single line changed. Neither one knows Riptides exists.
From here, checkout’s ordinary HTTP calls to orders-api, the exact same request the application always made, are transparently upgraded: the kernel on both nodes negotiates mutual TLS before a single byte of the actual request goes through, each side presenting a real SPIFFE X.509 certificate bound to the process that opened the connection, not to a proxy sitting next to it. Inspecting that certificate from either side shows a URI SAN of spiffe://acme.corp/demo/orders-api or spiffe://acme.corp/demo/checkout, a real SVID, not TLS for its own sake.
An SVID that nothing checks is just a certificate, though, so the same policy that issued it enforces it. Any process that doesn’t match either WorkloadIdentity, some unrelated pod that isn’t checkout, has no SVID to present and is rejected at the handshake. Not because a network policy happened to catch it, but because it was never issued an identity to begin with.
What that replaced
Going back to the component list earlier in this post: no SPIRE Server, no separate SPIRE Agent process, no CSI driver or SPIFFE-Helper sidecar to get a certificate onto the pod, because nothing is written to the pod’s filesystem at all. No separate policy engine or mesh, because the same enforcement point that issues the identity also checks it against policy before the connection completes.
And going back to the participation problem: neither orders-api nor checkout called a Workload API, linked a SPIFFE client library, or ran alongside a sidecar. Both were ordinary containers that happened to open a socket. That’s what makes the same model apply just as well to the legacy binary and the third-party appliance from earlier, they never have to become workloads that participate in their own identity delivery, because participation was never the mechanism to begin with.
The other two gaps close the same way. Day-one coverage isn’t capped by what’s been rewritten or fronted with a sidecar, because there’s nothing to rewrite or front: the kernel attests whatever’s already running, on day one, across the whole fleet at once. And a short-lived CI job doesn’t need anything set up for it in advance: the same WorkloadIdentity that covers a long-running service matches a runner that lives for ninety seconds just as well, the moment it opens a connection, not before.
Focus on what matters
The same SPIFFE IDs, the same SVIDs, verified against the same specification, issued and enforced straight from the kernel, with no SPIRE anywhere underneath it. No sidecars. No workload asked to participate in its own identity. No policy layer to bolt on afterward. Just a verified, enforced connection, from day one, on whatever you’re already running.
What this post doesn’t cover
Everything above is scoped to one thing on purpose: SPIFFE identity and kernel-enforced mTLS between two ordinary workloads. A few things adjacent to that are also part of Riptides, and worth naming so this doesn’t read as the whole picture, but each deserves its own post rather than a paragraph tacked on here.
Not everything a workload talks to speaks SPIFFE, and a lot of it never will: third-party SaaS APIs, cloud provider credentials, systems with their own established access model. Riptides brokers and injects credentials for those too, without the workload ever holding one, but that’s a different mechanism from the identity story in this post.
Every connection in the demo above is also a structured, queryable record: who connected to what, under which identity, allowed or denied and why. That’s a standing product capability, not a side effect of this particular walkthrough, and it’s owed more than a mention.
And everything here applies just as well to an AI agent as it does to orders-api, an agent is just another process that opens a socket, but agents raise their own set of questions on top: delegated user authorization, sub-agents spawned on the fly, tool-chain trust boundaries. That’s its own territory, covered separately.
Before you commit to building it
None of this means standing up the ecosystem is the wrong call for every team. Some organizations have the platform headcount, the timeline, and a genuine reason to need SPIRE’s specific attestation model, and for them, building it is exactly right. The honest way to tell which camp you’re in isn’t a demo. It’s answering three questions plainly, before the project has a name and a budget line.
How much of the fleet can actually be rewritten or fronted with a sidecar in the next two quarters, not eventually? Whatever falls outside that set runs with no identity in the meantime, and that leftover pile is almost always bigger than the first estimate.
Once a policy layer sits on top of identity, who owns keeping the two in step, permanently, as both keep changing? If there’s no name attached to that yet, it hasn’t been decided. It’s been postponed.
Is operating a distributed system, a server, a fleet of agents, a policy engine, a management UI, something the team is signing up to do indefinitely, on top of everything it already runs? That cost doesn’t end at rollout. It shows up on every on-call rotation after it.
If any of those three land uncomfortably, the fix isn’t rethinking whether SPIFFE is the right standard. It’s rethinking what has to run underneath it to get it.
Stuck with SPIRE? Let’s talk.
If you’re convinced of what the market already seems convinced of, that SPIFFE is where workload identity is headed, but the only path anyone’s shown you runs through SPIRE, the server, the agents, the policy layer, the participation requirement, that’s exactly the conversation worth having. Not because the standard is wrong. Because betting on SPIFFE and being stuck building SPIRE to get it are two different problems, and only one of them has to be true. Riptides doesn’t plug into an existing SPIRE deployment, it takes over identity issuance and enforcement entirely, down to the same SPIFFE IDs your workloads already carry, so nothing downstream has to change. Talk to us about what that migration actually looks like, or start free and see our approach for yourself.
How exposed are your workloads?
Run the NHI Security Audit Checklist, 8 questions to map your credential exposure, attribution gaps, and lateral movement surface across your own environment. Takes about 15 minutes.
Run the ChecklistFollow us on LinkedIn and X for more updates.