Workload Identity
A workload identity is the core primitive in Riptides. It binds a cryptographic identity (a SPIFFE ID backed by an X.509 certificate or JWT token) to a specific workload running on a node. The kernel module uses this binding to authenticate workloads on every connection, transparently and without application changes.
A WorkloadIdentity resource brings together everything that identity needs: how it’s matched to a running process, which node or fleet it’s scoped to, which peers it may talk to, and how its connections are secured. This page covers each of those in turn; for the exhaustive field-by-field spec, see the WorkloadIdentity reference.
SPIFFE IDs
Section titled “SPIFFE IDs”Every workload identity maps to a SPIFFE ID, a URI-formatted identifier within a trust domain:
spiffe://example.com/frontendspiffe://example.com/payment-serviceSPIFFE IDs are not tied to network addresses or hostnames. They represent the workload itself, regardless of where it runs. This matters in practice because IP addresses and hostnames get reassigned constantly: a redeploy, an autoscaling event, or a rescheduled pod can hand yesterday’s address to a completely different workload today. Policy written against an address is only as accurate as the last time someone confirmed it still points at the intended workload; policy written against a SPIFFE ID keeps meaning the same thing regardless of how many times the infrastructure underneath it changes.
Workloads prove their identity using SVIDs (SPIFFE Verifiable Identity Documents). Riptides supports two SVID types:
- X.509 SVIDs: Certificates used for mutual TLS. The SPIFFE ID is encoded in the certificate’s Subject Alternative Name (SAN) URI field. These are the primary identity credential for service-to-service communication.
- JWT SVIDs: Signed tokens used for federation with external systems. The SPIFFE ID appears in the
subclaim. These are typically used when exchanging workload identity for cloud provider credentials via OIDC.
Both types are issued automatically by the platform and rotated before expiration without workload restarts. The two exist side by side because they answer to different kinds of peers: a TLS-speaking service expects a certificate at the handshake, while a cloud IAM API or OIDC-federated system expects a signed bearer token it can exchange for credentials. Having both tied to the same underlying identity means a workload can authenticate over mTLS to internal services and separately present its identity to an external system, without maintaining two unrelated credentials for what is really one identity.
How Workloads Are Identified
Section titled “How Workloads Are Identified”Workload identification is performed by the daemon. When a process opens a new network connection, the daemon evaluates the connecting process against selectors defined in the WorkloadIdentity resource, using metadata collected from the process and its environment (Kubernetes pod labels, process names, cloud instance metadata, and more). A process must satisfy all selectors in at least one selector group to receive that identity.
Because processes restart, get rescheduled onto different nodes, and scale up and down continuously, this match is not a one-time assignment made when the WorkloadIdentity is created; it is re-evaluated on every new connection. The WorkloadIdentity is declared once, and the daemon assigns it to whichever process currently fits the description, wherever that process happens to be running right now.
See Workload Attestation for the complete selector matching rules and the full list of metadata keys available from every collector.
Quick example
Section titled “Quick example”A Next.js frontend running in Kubernetes might be identified by combining two selectors:
apiVersion: core.riptides.io/v1alpha1kind: WorkloadIdentitymetadata: name: frontend namespace: riptides-systemspec: workloadID: frontend scope: daemonGroup: id: daemongroup/prod-us-east-1/workers selectors: - k8s:label:app: frontend process:cmdline: node server.jsOnly a process whose command line matches node server.js, running inside a pod with the label app=frontend, will receive the spiffe://example.com/frontend identity.
Identity Lifecycle
Section titled “Identity Lifecycle”Certificates are short-lived by default, and their entire lifecycle, issuance, rotation, and revocation, happens without the application ever handling a private key, a certificate file, or a restart. The kernel module itself drives this: it keeps a cache of keys and certificates and, on every new connection, decides on the spot whether what it’s holding is still good or needs to be requested fresh, going to the daemon and control plane only when it actually needs something from them. That removes the two places certificate lifecycle management usually breaks down in practice: an app team hand-rolling its own refresh logic, or a certificate quietly expiring because nobody wired up rotation for a workload that predates its current owner.
Issuance
Section titled “Issuance”When a new connection needs an identity the kernel module doesn’t already have cached, it generates a private key in kernel memory and sends a certificate signing request (CSR) to the daemon, which forwards it to the control plane. The resulting X.509 SVID is loaded back into the kernel module and cached for reuse.
Rotation
Section titled “Rotation”On each new connection, the kernel module checks the cached certificate’s remaining lifetime and, if it’s due to expire soon, requests a fresh one the same way, in the background. Existing connections keep using their already-negotiated session and are unaffected; no workload restart is required.
Revocation
Section titled “Revocation”There’s no separate revoke step to run. The cache is finite, so entries for workloads that no longer match a WorkloadIdentity’s selectors, or that simply haven’t been needed in a while, age out on their own. Once a cached identity is gone, the next connection for it starts over at issuance rather than presenting something stale.
Every WorkloadIdentity is scoped to exactly one of the following:
- Daemon scope: The identity applies to workloads on a single, named node. This is the default when a daemon operates independently.
- DaemonGroup scope: The identity applies to every daemon in a group, so a service that runs many replicas across a fleet needs only one WorkloadIdentity definition rather than one per node.
spec: scope: daemon: id: riptides/daemon/my-first-node# orspec: scope: daemonGroup: id: daemongroup/prod-us-east-1/workersScope decides which daemon or daemons the control plane distributes this policy to at all, before selector matching ever runs locally on a given node. DaemonGroup scope is what lets one definition follow a horizontally-scaled service across however many nodes it’s currently running on, without being redeclared each time a node joins or leaves the fleet; Daemon scope is the right fit when a workload is genuinely pinned to one specific machine.
Access Control
Section titled “Access Control”allowedSPIFFEIDs restricts which peers a workload is allowed to talk to. It has two directions:
inbound: SPIFFE IDs allowed to connect to this workload.outbound: SPIFFE IDs this workload is allowed to connect to.
spec: allowedSPIFFEIDs: inbound: - spiffe://example.com/myapp/frontend outbound: - spiffe://example.com/myapp/orders-dbIf allowedSPIFFEIDs is omitted entirely, any workload in the trust domain that satisfies the connection’s TLS mode requirement can connect, in either direction. TLS mode alone only proves a peer holds a valid, cryptographically verifiable identity somewhere in the trust domain; it says nothing about whether that specific peer should be talking to this workload. allowedSPIFFEIDs is where that second check happens, keeping “is this identity valid” separate from “is this exact identity allowed here” so you can tighten access per workload, per destination, or per port without changing how identity itself is proven. See Connection Security for how TLS mode (MUTUAL, SIMPLE, PERMISSIVE) and allowedSPIFFEIDs interact at the handshake, and for restricting unauthenticated access to specific HTTP paths with httpRequestPath.
The top-level allowedSPIFFEIDs is a default. egress[].allowedSPIFFEIDs and ingress[].allowedSPIFFEIDs replace it (they don’t add to it) for that specific egress rule or ingress port, letting you set a permissive default and lock down individual destinations or ports, or vice versa.
Egress Filtering
Section titled “Egress Filtering”Egress rules control outbound connections. Each rule targets one or more Services by label selector rather than by naming individual Service resources, and can override the default connection policy, allowed SPIFFE IDs, SVID settings, and workload ID for traffic to those services:
spec: egress: - selectors: - tier: backend connection: tls: mode: MUTUAL allowedSPIFFEIDs: - spiffe://example.com/orders-service - spiffe://example.com/inventory-serviceThis allows the workload to reach any Service labeled tier: backend, over mutual TLS, but only if the remote side presents one of the two listed SPIFFE IDs. Because matching is by label rather than by naming each Service resource individually, this egress rule doesn’t need to change every time a new Service is added to the group it already trusts: label the new Service tier: backend and it’s covered without touching this WorkloadIdentity.
A workload with no matching egress rule for a given destination connects using the top-level connection and allowedSPIFFEIDs settings.
Ingress Filtering
Section titled “Ingress Filtering”Ingress rules control inbound connections, per listening port:
spec: ingress: - port: 8443 connection: tls: mode: MUTUAL allowedSPIFFEIDs: - spiffe://example.com/myapp/frontend - spiffe://example.com/monitoring - port: 8080 connection: protocol: HTTP1 tls: mode: PERMISSIVE httpRequestPath: - /healthzHere, port 8443 requires mutual TLS from one of two callers, while port 8080 accepts plaintext but only forwards unauthenticated requests to /healthz; every other path on that port still requires a verified identity. Not every port on a workload needs the same trust requirement: a health-check port has to accept probes from a kubelet or load balancer that carries no workload identity at all, while an application port handling real traffic should require mutual TLS. Per-port rules let both live on the same workload without weakening the strict port, or standing up a separate proxy just to translate unauthenticated probe traffic into something that port would otherwise accept. See Connection Security for the full httpRequestPath semantics.
A port with no matching ingress rule falls back to the top-level connection and allowedSPIFFEIDs settings.
Examples
Section titled “Examples”Restricting inbound callers
Section titled “Restricting inbound callers”A payment service that only accepts connections from two specific workloads, rejecting everything else at the TLS handshake:
apiVersion: core.riptides.io/v1alpha1kind: WorkloadIdentitymetadata: name: payment-service namespace: riptides-systemspec: workloadID: myapp/payment-service scope: daemonGroup: id: daemongroup/prod-us-east-1/workers selectors: - k8s:label:app: payment-service connection: tls: mode: MUTUAL allowedSPIFFEIDs: inbound: - spiffe://example.com/myapp/checkout - spiffe://example.com/myapp/billingPer-service egress overrides
Section titled “Per-service egress overrides”A workload that reaches internal services over mutual TLS by default, but connects to one specific external analytics endpoint over simple TLS instead:
apiVersion: core.riptides.io/v1alpha1kind: WorkloadIdentitymetadata: name: checkout namespace: riptides-systemspec: workloadID: myapp/checkout scope: daemonGroup: id: daemongroup/prod-us-east-1/workers selectors: - k8s:label:app: checkout connection: tls: mode: MUTUAL egress: - selectors: - tier: backend connection: tls: mode: MUTUAL - selectors: - service: external-analytics connection: tls: mode: SIMPLEHealth probes alongside a strict application port
Section titled “Health probes alongside a strict application port”An API server that requires mutual TLS on its application port but lets an unauthenticated kubelet probe reach /healthz on a separate port:
apiVersion: core.riptides.io/v1alpha1kind: WorkloadIdentitymetadata: name: api-server namespace: riptides-systemspec: workloadID: myapp/api-server scope: daemonGroup: id: daemongroup/prod-us-east-1/workers selectors: - k8s:label:app: api-server connection: tls: mode: MUTUAL ingress: - port: 8080 connection: tls: mode: MUTUAL allowedSPIFFEIDs: - spiffe://example.com/myapp/frontend - port: 9000 connection: protocol: HTTP1 tls: mode: PERMISSIVE httpRequestPath: - /healthz - /readyz