Skip to content

Daemon Deployment on Kubernetes

This guide covers deploying the Riptides daemon as a DaemonSet on Kubernetes clusters. The daemon runs on every node, loads the Riptides kernel module, and connects to the control plane for identity issuance and policy enforcement.

  • Kubernetes 1.24+
  • Helm 3.2.0+
  • Privileged container support (required for kernel module loading)
  • On Azure, use an AKS Standard cluster. AKS Automatic blocks the daemon by default; see AKS Automatic: daemon blocked by admission policies under Troubleshooting.
  • Network connectivity from workload nodes to the control plane’s API, gRPC, and tunnel endpoints (HTTPS/TLS on port 443)
  • A running Riptides control plane (see Control Plane Deployment)

Install the daemon using the Riptides OCI Helm chart:

Terminal window
helm install riptides-daemon oci://ghcr.io/riptides-packages/helm/daemon --version version \
--namespace riptides-system \
--create-namespace \
-f daemon-values.yaml

The daemon Helm chart deploys a DaemonSet that runs on every node in your cluster. Each pod consists of:

  1. Driver loader init container: loads the Riptides kernel module onto the host before the daemon starts. The init container runs privileged and uses a pre-built image matching your kernel version and distribution.

  2. Daemon container: the main daemon process that communicates with the kernel module (via /dev/riptides), connects to the control plane, manages certificates, and applies policies.

All daemon settings live under config.daemon:

ParameterDescriptionDefault
config.daemon.trustDomainSPIFFE trust domain (must match control plane)example.com
config.daemon.defaultCertTTLDefault TTL for issued workload certificates2h
config.daemon.dataDirPersistent data directory inside the container/data/riptides
config.daemon.kernelModuleDevicePath to the kernel module character device/host/dev/riptides
config.daemon.ephemeralAuto-remove this daemon after it stops heart-beating. Overrides the control-plane attestation default; omit to let the control plane decide. Useful for daemons on autoscaled/spot nodes.(unset)

Settings under config.daemon.controlPlane:

ParameterDescriptionDefault
config.daemon.controlPlane.enabledEnable control plane connectiontrue
config.daemon.controlPlane.urlHTTPS base URL of the control planehttps://controlplane:8443
config.daemon.controlPlane.grpcServerAddressgRPC endpoint (host:port)none
config.daemon.controlPlane.tokenBrokerBaseURLToken broker endpoint URLnone
config.daemon.controlPlane.allowInsecureConnectionSkip TLS verification (development only)false
config.daemon.controlPlane.authPlugin.typeAuthentication plugin type (see below)joinToken

When using separate DNS records for each control plane service (typical in production), configure all three endpoints:

config:
daemon:
controlPlane:
url: https://cp.example.com
grpcServerAddress: grpc.example.com:443
tokenBrokerBaseURL: https://cp.example.com/token-broker

Configure a tunnel connection so the control plane can retrieve telemetry from the daemon in a pull model:

ParameterDescriptionDefault
config.daemon.tunnelServer.addressTunnel server endpoint (host:port)none
config:
daemon:
tunnelServer:
address: tunnel.example.com:443

The daemon authenticates to the control plane using one of these plugins:

Recommended for DaemonSets. Since the daemon runs as one pod per node, K8sSAT lets every pod authenticate on its own: the Helm chart automatically projects a ServiceAccount token into the pod (audience riptides) when this plugin is selected, so there’s no secret to create or distribute across nodes.

config:
daemon:
controlPlane:
authPlugin:
type: K8sSAT
config:
clusterID: "your-cluster-id"

Create the corresponding Verifier on the control plane once for the whole cluster:

apiVersion: auth.riptides.io/v1alpha1
kind: Verifier
metadata:
name: k8s-cluster
namespace: riptides-system
spec:
K8sSAT:
clusterID: "your-cluster-id"
audience: riptides
jwksSource:
remote:
url: "https://your-k8s-api-server/openid/v1/jwks"

clusterID is an arbitrary identifier you choose — it just has to match between the Verifier and the daemon config. See Attestation and Selectors for details on the JWKS source.

A single-use, pre-shared token: the control plane deletes it as soon as one daemon authenticates with it. This makes it a good fit for a one-off manual attach (a single VM or bare-metal host), but not for a DaemonSet — every pod would need its own distinct token, since only the first pod to present a given token succeeds and the rest fail authentication. Use K8sSAT (above) for DaemonSets instead.

config:
daemon:
controlPlane:
authPlugin:
type: joinToken
config:
token: "your-join-token-here"

Create the corresponding resources on the control plane:

apiVersion: auth.riptides.io/v1alpha1
kind: Verifier
metadata:
name: jointoken
namespace: riptides-system
spec:
joinToken: {}
---
apiVersion: auth.riptides.io/v1alpha1
kind: JoinToken
metadata:
name: my-join-token
namespace: riptides-system
spec:
token: "your-join-token-here"

Recommended for AWS deployments. The daemon uses the EC2 instance identity document for attestation, so no shared secrets are required:

config:
daemon:
controlPlane:
authPlugin:
type: AWSIID

Recommended for GCP deployments. Uses the GCP instance identity token:

config:
daemon:
controlPlane:
authPlugin:
type: GCPIIT

Metadata collectors enrich workload identity information with context from the runtime environment. Configure under config.daemon.metadataCollectors:

CollectorDescriptionDefault
procfsCollects process information from /procenabled: true
procfs.extractEnvsAlso collect environment variablesfalse
linuxosCollects OS release informationenabled: true
sysfsdmiCollects DMI/SMBIOS hardware infoenabled: true
ec2AWS EC2 instance metadataenabled: true
gcpGCP instance metadataenabled: true
azureAzure instance metadataenabled: true
kubernetesKubernetes pod and node metadataenabled: true
dockerDocker container metadataenabled: false

For the Kubernetes metadata collector:

ParameterDescriptionDefault
kubernetes.kubeletHostKubelet API hostlocalhost
kubernetes.kubeletPortKubelet API port10250
kubernetes.kubeletCAPath to kubelet CA certificate/etc/kubernetes/pki/ca.crt
kubernetes.skipKubeletVerificationSkip kubelet TLS verificationtrue
kubernetes.credentialsPath to service account token/var/run/secrets/kubernetes.io/serviceaccount/token

Disable cloud provider collectors that do not apply to your environment to avoid unnecessary metadata API calls:

config:
daemon:
metadataCollectors:
ec2:
enabled: true
gcp:
enabled: false
azure:
enabled: false

The driver loader init container loads the Riptides kernel module before the daemon starts:

ParameterDescriptionDefault
driverLoader.image.repositoryDriver loader imageghcr.io/riptides-packages/images/driver-loader
driverLoader.image.pullPolicyPull policyIfNotPresent
driverLoader.driverVersionDriver version (git branch, tag, or commit)main
driverLoader.commandTimeoutTimeout in seconds for load commands5
driverLoader.dynDBGEnable kernel dynamic debug for the modulefalse
ParameterDescriptionDefault
healthStatus.portDaemon health check port10100

A complete daemon-values.yaml for an AWS EKS cluster:

image:
tag: "v0.1.0"
pullPolicy: Always
driverLoader:
image:
tag: "v0.5.2"
driverVersion: "v0.1.0"
commandTimeout: 5
dynDBG: false
config:
daemon:
trustDomain: example.com
defaultCertTTL: 2h
dataDir: /data/riptides
metadataCollectors:
procfs:
enabled: true
extractEnvs: false
linuxos:
enabled: true
sysfsdmi:
enabled: true
ec2:
enabled: true
gcp:
enabled: false
azure:
enabled: false
kubernetes:
enabled: true
kubeletHost: localhost
kubeletPort: 10250
kubeletCA: /etc/kubernetes/pki/ca.crt
skipKubeletVerification: true
credentials: /var/run/secrets/kubernetes.io/serviceaccount/token
docker:
enabled: false
controlPlane:
enabled: true
url: https://cp.example.com
grpcServerAddress: grpc.example.com:443
tokenBrokerBaseURL: https://cp.example.com/token-broker
authPlugin:
type: AWSIID
tunnelServer:
address: tunnel.example.com:443
healthStatus:
port: 10100
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
tolerations:
- operator: Exists

Check that daemon pods are running on all nodes:

Terminal window
kubectl get pods -n riptides-system -l app.kubernetes.io/name=daemon -o wide

Check the driver loader init container logs to confirm the kernel module loaded:

Terminal window
kubectl logs -n riptides-system -l app.kubernetes.io/name=daemon -c daemon-driver-loader

Check the daemon logs for control plane connectivity:

Terminal window
kubectl logs -n riptides-system -l app.kubernetes.io/name=daemon -f

Verify health:

Terminal window
kubectl port-forward -n riptides-system daemonset/riptides-daemon 10100:10100
curl http://localhost:10100/healthz

If the init container fails, the kernel module loader may have failed. Check the kernel module loader init container.

Terminal window
kubectl logs -n riptides-system <failed-daemon-pod> -c daemon-driver-loader

Verify network connectivity from a workload node to the control plane endpoints:

Terminal window
kubectl run -n riptides-system netcheck --rm -it --image=busybox -- \
wget -qO- --timeout=5 https://cp.example.com/healthz

Ensure your security groups or network policies allow outbound HTTPS/TLS (port 443) traffic to the control plane’s API (cp.example.com), gRPC (grpc.example.com), and tunnel (tunnel.example.com) endpoints.

Check that the trust domain in the daemon configuration exactly matches the trust domain configured on the control plane. For join token auth, verify the token value matches the JoinToken resource on the control plane — and remember a JoinToken is deleted after its first successful use, so if multiple pods (e.g. a DaemonSet) are configured with the same token, only the first one to start will succeed; the rest will fail with “invalid join token”. Switch to K8sSAT (above) if that’s what’s happening.

AKS Automatic: daemon blocked by admission policies

Section titled “AKS Automatic: daemon blocked by admission policies”

On AKS Automatic clusters, helm install fails with admission errors such as:

ValidatingAdmissionPolicy 'aks-managed-baseline-host-namespaces' denied request:
Host network namespaces are disallowed: spec.hostNetwork is set to true
ValidatingAdmissionPolicy 'aks-managed-block-nodes-proxy-rbac' denied request:
Creating RBAC rules that grant nodes/proxy access is not allowed.

The daemon is a privileged, host-network DaemonSet that loads a kernel module. AKS Automatic turns on Kubernetes guardrails by default (Deployment Safeguards with baseline Pod Security Standards, plus managed admission policies), and they reject what the daemon needs: hostNetwork, privileged containers, hostPath mounts, and nodes/proxy RBAC.

The supported option is an AKS Standard cluster, where these policies are off by default and the daemon installs without changes.

If you have to run on AKS Automatic, exclude the daemon’s namespace from Deployment Safeguards:

Terminal window
az aks safeguards update --resource-group <rg> --name <cluster> \
--level Enforce --excluded-ns riptides-system

Even then, some limits remain:

  • Namespace exclusion is all or nothing. It removes every baseline Pod Security protection for that namespace, not only the ones the daemon needs.
  • The nodes/proxy policy has no namespace exclusion, so the daemon’s nodes/proxy ClusterRole can’t be exempted and must not be created. That grant is only used by the direct-kubelet metadata path. The default metadata collection talks to the Kubernetes API server and doesn’t need it, so leave the metadataCollectors.kubernetes kubelet settings at their defaults. The chart has no flag to drop this ClusterRole on its own (it is created along with serviceAccount.create), so strip it with a Helm --post-renderer patch, or from the rendered manifests, until the chart adds an option.
  • Managed node pools can still block loading an out-of-tree kernel module, so the driver may fail to load even after you exclude the namespace.
Terminal window
helm upgrade riptides-daemon oci://ghcr.io/riptides-packages/helm/daemon \
--namespace riptides-system \
-f daemon-values.yaml

The DaemonSet performs a rolling update by default. The driver loader init container will reload the kernel module on each node as pods restart.