Skip to content

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.

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:

  1. Create a WorkloadIdentity for the backend API with inbound SPIFFE ID restrictions
  2. Create a WorkloadIdentity for the frontend app with egress rules targeting the backend
  3. Create a Service representing the backend endpoint
  4. Verify that the frontend connects over mTLS without application changes
  • 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
  • kubectl configured with access to the riptides-system namespace

You have two services in the myapp namespace:

ServiceDescriptionAddressPort
frontendWeb application serving end usersfrontend.myapp.svc.cluster.local8080
backendREST API consumed by the frontendbackend-api.myapp.svc.cluster.local3000

The goal: only the frontend may call the backend, and the backend must verify the frontend’s identity via mTLS.

Define a Riptides Service resource that tells the control plane about the backend API’s network address.

apiVersion: core.riptides.io/v1alpha1
kind: Service
metadata:
name: backend-api-svc
namespace: riptides-system
spec:
addresses:
- address: backend-api.myapp.svc.cluster.local
port: 3000
labels:
app: myapp
service: backend-api

Apply it:

Terminal window
riptides-cli ctl apply -f backend-api-svc.yaml

The 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/v1alpha1
kind: WorkloadIdentity
metadata:
name: backend-api
namespace: riptides-system
spec:
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/frontend

Apply it:

Terminal window
riptides-cli ctl apply -f backend-api-workloadidentity.yaml

Key fields explained:

  • workloadID: Becomes the SPIFFE ID spiffe://example.com/myapp/app/backend-api (where example.com is 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/v1alpha1
kind: WorkloadIdentity
metadata:
name: frontend
namespace: riptides-system
spec:
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-api

Apply it:

Terminal window
riptides-cli ctl apply -f frontend-workloadidentity.yaml

Key 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 the labels on 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.

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 it
const response = await fetch('http://backend-api.myapp.svc.cluster.local:3000/api/data');

Under the hood:

  1. The frontend process opens a TCP connection to backend-api.myapp.svc.cluster.local:3000
  2. The Riptides kernel module intercepts the outgoing connection
  3. The kernel matches the destination against the egress rules and initiates an mTLS handshake
  4. Both sides exchange X.509-SVID certificates containing their SPIFFE IDs
  5. The backend kernel verifies the client certificate against allowedSPIFFEIDs.inbound
  6. The frontend kernel verifies the server certificate against the egress allowedSPIFFEIDs
  7. Encrypted data flows between the two services

Riptides supports three TLS modes. Choose the right one based on your migration stage and security requirements.

ModeBehaviorWhen to Use
MUTUALBoth 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.
PERMISSIVEThe 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.
SIMPLEThe 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:

  1. Start with PERMISSIVE on all services so existing traffic is unaffected
  2. Monitor Riptides telemetry to confirm all callers are using mTLS
  3. Switch to MUTUAL to enforce certificate-based authentication

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 connect
allowedSPIFFEIDs:
inbound:
- spiffe://example.com/myapp/app/frontend
- spiffe://example.com/monitoring/app/healthcheck

Outbound 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 backend
allowedSPIFFEIDs:
outbound:
- spiffe://example.com/myapp/app/backend-api

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/redis

Together, 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.