Waterwheel
Workload identity for Kubernetes. Your pods call AI and cloud APIs with short-lived tokens, and never hold a key.
An API key in a pod can be taken by anyone who gets into the pod. It sits in an environment variable or a Secret, where every process, every kubectl exec and every leaked backup can read it.
Anthropic, OpenAI and the cloud providers now offer workload identity federation instead. The workload proves who it is with a signed token and gets a short-lived one back. Every pod already has such a token: Kubernetes signs one for its service account. Waterwheel uses it for every pod, without changing the app.
Presented at KCD Budapest
Section titled “Presented at KCD Budapest”We presented the idea at KCD Budapest on 30 October 2026. In the live demo, a pod with no key called Claude: SPIRE gave it an identity, tokenex exchanged that for a short-lived Anthropic token, and an egress sidecar added the token on the way out.
Waterwheel turns that demo into a product. By default the pod’s service account token takes SPIRE’s place, so there’s nothing extra to install. SPIRE is still an option, and the demo’s five steps run as an automated test with both.
Every Workload Deserves an Identity Instead of an API Key
Zsolt Varga and Nándor Krácser, Riptides
A live demo on kind: a pod with no key calls Claude, through SPIRE, tokenex and an egress sidecar.
- 1The pod has no key
- 2The API call fails
- 3Turn on the egress proxy
- 4The same command succeeds
- 5Nothing to steal
How it works
Section titled “How it works”The example is support-bot, a Python service in the support namespace that answers customer tickets with Claude through Anthropic’s Python SDK.
The pod’s identity comes from its Kubernetes service account token, the default, or from SPIRE. Everything else works the same way.
Waterwheel’s components have teal outlines. The app never sees a key or a token: the service account tokens are mounted only into the sidecar, and the Anthropic token only exists in its memory.
-
The kubelet writes the pod’s service account tokens, signed by the API server, into a volume only
waterwheel-proxymounts. There’s one per audience the pod’s bindings need, and the kubelet replaces them before they expire. -
waterwheel-proxy, a sidecar in the same pod, askswaterwheel-controllerfor its bindings. It proves who it is with its token, which the controller checks with a TokenReview. -
The app sends its request through
HTTPS_PROXY, which the injector set. It has no key. -
The proxy exchanges the service account token at Anthropic for a short-lived token.
-
Anthropic checks the token’s signature against the cluster’s published keys, and that its subject is
system:serviceaccount:support:support-bot. -
The proxy adds the token to the request and sends it on.
Waterwheel’s components have teal outlines. The app never sees a key, a token or the SPIRE socket: the Workload API is mounted only into the sidecar, and the tokens only exist in its memory.
-
waterwheel-controllerregisters the pod’sWorkloadIdentitywith SPIRE, as ClusterSPIFFEIDs. Its selectors pick the pods; the most specific match wins. -
The SPIRE agent on the node attests
waterwheel-proxyand issues it JWT-SVIDs, for any audience, through a Workload API socket only the sidecar mounts. -
The proxy asks
waterwheel-controllerfor its bindings, which also tell it the pod’s SPIFFE ID. It proves who it is with a service account token, checked with a TokenReview. -
The app sends its request through
HTTPS_PROXY, which the injector set. It has no key. -
The proxy exchanges a JWT-SVID at Anthropic for a short-lived token. Anthropic checks it against SPIRE’s published keys, and that its subject is
spiffe://example.org/ns/support/support-bot. -
The proxy adds the token to the request and sends it on.
The example in code
Section titled “The example in code”The app
Section titled “The app”Ordinary SDK code, with no Waterwheel library and no key. The SDK picks up the proxy and the CA bundle from HTTPS_PROXY and SSL_CERT_FILE, which the injector sets.
import anthropic
# There's no API key in the pod. The SDK needs some value to send;# waterwheel-proxy removes it and adds a short-lived token instead.client = anthropic.Anthropic(api_key="unused")
def answer(ticket: str) -> str: response = client.beta.messages.create( model="claude-opus-5-5", max_tokens=16000, betas=["server-side-fallback-2026-07-01"], fallbacks="default", system="You answer customer support tickets for Example Corp. Be brief and friendly.", messages=[{"role": "user", "content": ticket}], ) if response.stop_reason == "refusal": return "A person from our team will follow up on this one." return "".join(block.text for block in response.content if block.type == "text")The Waterwheel objects
Section titled “The Waterwheel objects”Two objects say which credential the pod’s service account gets, and for which destinations. A namespace label turns the sidecar on.
apiVersion: core.riptides.io/v1alpha1kind: CredentialSourcemetadata: {name: anthropic, namespace: support}spec: anthropic: organizationId: f5a4ab71-21b3-44b8-a3fb-54097ce876ad workspaceId: default serviceAccountId: svac_01WDRm8rfFaGYa5VVFzEYlF0 federationRuleId: fdrl_01Hx1vufYmRP9iyeD7UdKvyZ---apiVersion: core.riptides.io/v1alpha1kind: CredentialBindingmetadata: {name: support-bot-anthropic, namespace: support}spec: workloadID: ns/support/sa/support-bot # pods running as this service account credentialSource: anthropic propagation: injection: destinations: - host: api.anthropic.comkubectl label namespace support waterwheel.riptides.io/inject=enabledWhat you get
Section titled “What you get”No keys in pods
The app container gets no key, no token and no socket. A shell in the pod finds nothing to steal.
Nothing extra to run
Kubernetes signs and rotates the identity tokens, so there’s no agent on the nodes. With SPIRE, WorkloadIdentity selectors give pods that share a service account their own SPIFFE IDs.
Namespaces are the boundary
A binding only applies to service accounts in its own namespace. This is checked at admission and again when the controller hands out bindings.
Many token endpoints
Anthropic, OpenAI, Google Cloud and Microsoft Entra ID (Azure) federation, and any RFC 7523 or RFC 8693 endpoint. AWS is next.
Trust without setup
The sidecar’s CA goes into the pod’s system trust store and the usual environment variables, so curl, Python, Node.js and Go accept it.
Open source
Apache-2.0. Token exchange runs on tokenex, our open-source exchange library.
Waterwheel is built by Riptides.