Setting Up mTLS Between Services
This guide walks through securing communication between two internal services using mutual TLS (mTLS) with Riptides. By the end, your frontend application will connect to a backend API over an encrypted, mutually authenticated channel — without any code changes to either service.
Overview
Section titled “Overview”Riptides uses a kernel module to transparently intercept network traffic and upgrade it to mTLS. Your applications continue to make plain HTTP calls on localhost or internal addresses, while the kernel handles certificate exchange, encryption, and identity verification underneath.
In this guide you will:
- Create a WorkloadIdentity for the backend API with inbound SPIFFE ID restrictions
- Create a WorkloadIdentity for the frontend app with egress rules targeting the backend
- Create a Service representing the backend endpoint
- Verify that the frontend connects over mTLS without application changes
Prerequisites
Section titled “Prerequisites”- A running Riptides daemon group (this guide uses
riptides/daemongroup/dev-eu-west-1/on-demand-workers) - Both workloads deployed to a Kubernetes cluster with the Riptides daemon installed
kubectlconfigured with access to theriptides-systemnamespace
Scenario
Section titled “Scenario”You have two services in the myapp namespace:
| Service | Description | Address | Port |
|---|---|---|---|
| frontend | Web application serving end users | frontend.myapp.svc.cluster.local | 8080 |
| backend | REST API consumed by the frontend | backend-api.myapp.svc.cluster.local | 3000 |
The goal: only the frontend may call the backend, and the backend must verify the frontend’s identity via mTLS.
Step 1: Create the Backend Service
Section titled “Step 1: Create the Backend Service”Define a Riptides Service resource that tells the control plane about the backend API’s network address.
apiVersion: core.riptides.io/v1alpha1kind: Servicemetadata: name: backend-api-svc namespace: riptides-systemspec: addresses: - address: backend-api.myapp.svc.cluster.local port: 3000 labels: app: myapp service: backend-apiApply it:
riptides-cli ctl apply -f backend-api-svc.yamlThe labels field is important — egress rules on the frontend’s WorkloadIdentity will use these labels as selectors to match this Service.
Step 2: Create the Backend WorkloadIdentity
Section titled “Step 2: Create the Backend WorkloadIdentity”The backend’s WorkloadIdentity defines its SPIFFE identity, how inbound connections are handled, and which callers are permitted.
apiVersion: core.riptides.io/v1alpha1kind: WorkloadIdentitymetadata: name: backend-api namespace: riptides-systemspec: workloadID: myapp/app/backend-api selectors: - k8s:label:app: backend-api k8s:pod:namespace: myapp process:name: node scope: daemonGroup: id: riptides/daemongroup/dev-eu-west-1/on-demand-workers connection: tls: mode: MUTUAL allowedSPIFFEIDs: inbound: - spiffe://example.com/myapp/app/frontendApply it:
riptides-cli ctl apply -f backend-api-workloadidentity.yamlKey fields explained:
workloadID: Becomes the SPIFFE IDspiffe://example.com/myapp/app/backend-api(whereexample.comis your trust domain).selectors: Match criteria that bind this identity to running pods. The daemon verifies the pod label, namespace, and process name before issuing an identity.connection.tls.mode: MUTUAL: Requires all inbound connections to present a valid client certificate.allowedSPIFFEIDs.inbound: Only the frontend’s SPIFFE ID is authorized. Any other workload attempting to connect will be rejected.
Step 3: Create the Frontend WorkloadIdentity
Section titled “Step 3: Create the Frontend WorkloadIdentity”The frontend needs its own identity and an egress rule that targets the backend Service.
apiVersion: core.riptides.io/v1alpha1kind: WorkloadIdentitymetadata: name: frontend namespace: riptides-systemspec: workloadID: myapp/app/frontend selectors: - k8s:label:app: frontend k8s:pod:namespace: myapp process:name: node scope: daemonGroup: id: riptides/daemongroup/dev-eu-west-1/on-demand-workers connection: tls: mode: PERMISSIVE allowedSPIFFEIDs: outbound: - spiffe://example.com/myapp/app/backend-api egress: - selectors: - app: myapp service: backend-api connection: tls: mode: MUTUAL allowedSPIFFEIDs: - spiffe://example.com/myapp/app/backend-apiApply it:
riptides-cli ctl apply -f frontend-workloadidentity.yamlKey fields explained:
connection.tls.mode: PERMISSIVE: The frontend accepts both plaintext and TLS inbound connections (useful when it sits behind a load balancer or ingress controller that terminates TLS).allowedSPIFFEIDs.outbound: Restricts which SPIFFE IDs the frontend is allowed to connect to. If the backend presented a different identity, the connection would fail.egress: Defines outbound connection policies. Each entry matches a Service by its labels (selectors) and specifies the TLS mode and allowed peer identities for that destination.selectors: Match thelabelson the backend Service resource from Step 1.connection.tls.mode: MUTUAL: The kernel will perform a full mTLS handshake when connecting to this destination.allowedSPIFFEIDs: The frontend verifies the backend presents this exact SPIFFE ID.
Step 4: Verify Transparent mTLS
Section titled “Step 4: Verify Transparent mTLS”Your frontend application does not need any changes. It continues to make plain HTTP requests to backend-api.myapp.svc.cluster.local:3000:
// No TLS configuration needed -- the kernel handles itconst response = await fetch('http://backend-api.myapp.svc.cluster.local:3000/api/data');Under the hood:
- The frontend process opens a TCP connection to
backend-api.myapp.svc.cluster.local:3000 - The Riptides kernel module intercepts the outgoing connection
- The kernel matches the destination against the egress rules and initiates an mTLS handshake
- Both sides exchange X.509-SVID certificates containing their SPIFFE IDs
- The backend kernel verifies the client certificate against
allowedSPIFFEIDs.inbound - The frontend kernel verifies the server certificate against the egress
allowedSPIFFEIDs - Encrypted data flows between the two services
TLS Mode Reference
Section titled “TLS Mode Reference”Riptides supports three TLS modes. Choose the right one based on your migration stage and security requirements.
| Mode | Behavior | When to Use |
|---|---|---|
MUTUAL | Both client and server must present valid X.509-SVID certificates. Connections without certificates are rejected. | Production workloads where both sides are managed by Riptides. This is the strongest security posture. |
PERMISSIVE | The server presents a certificate and accepts mTLS if the client offers one, but also allows plaintext connections. | During migration, when some callers are not yet enrolled in Riptides. Also useful for health checks and load balancer probes that cannot present certificates. |
SIMPLE | The server presents a certificate (server-side TLS), but the client does not authenticate. Similar to standard HTTPS. | Connecting to external services that support TLS but not SPIFFE-based mTLS. |
A common migration path:
- Start with PERMISSIVE on all services so existing traffic is unaffected
- Monitor Riptides telemetry to confirm all callers are using mTLS
- Switch to MUTUAL to enforce certificate-based authentication
How allowedSPIFFEIDs Restrict Connections
Section titled “How allowedSPIFFEIDs Restrict Connections”The allowedSPIFFEIDs field provides bidirectional access control based on cryptographic identity:
Inbound restrictions (allowedSPIFFEIDs.inbound)
Section titled “Inbound restrictions (allowedSPIFFEIDs.inbound)”Defined on the server (the service receiving connections). Only workloads whose SPIFFE IDs appear in this list can establish connections. All other callers are rejected at the TLS handshake.
# On the backend: only the frontend and a monitoring service may connectallowedSPIFFEIDs: inbound: - spiffe://example.com/myapp/app/frontend - spiffe://example.com/monitoring/app/healthcheckOutbound restrictions (allowedSPIFFEIDs.outbound)
Section titled “Outbound restrictions (allowedSPIFFEIDs.outbound)”Defined on the client (the service initiating connections). The kernel verifies the server’s SPIFFE ID during the TLS handshake and drops the connection if it does not match.
# On the frontend: only allow connecting to the backendallowedSPIFFEIDs: outbound: - spiffe://example.com/myapp/app/backend-apiEgress-level restrictions
Section titled “Egress-level restrictions”Each egress entry can also specify allowedSPIFFEIDs for fine-grained, per-destination control:
egress: - selectors: - app: myapp service: backend-api connection: tls: mode: MUTUAL allowedSPIFFEIDs: - spiffe://example.com/myapp/app/backend-api - selectors: - app: cache connection: tls: mode: MUTUAL allowedSPIFFEIDs: - spiffe://example.com/myapp/app/redisTogether, these restrictions enforce a zero-trust network model where every connection is authenticated and authorized based on cryptographic workload identity — all without any application-level changes.