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.
Prerequisites
Section titled “Prerequisites”- 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)
Installation
Section titled “Installation”Install the daemon using the Riptides OCI Helm chart:
helm install riptides-daemon oci://ghcr.io/riptides-packages/helm/daemon --version version \ --namespace riptides-system \ --create-namespace \ -f daemon-values.yamlHow It Works
Section titled “How It Works”The daemon Helm chart deploys a DaemonSet that runs on every node in your cluster. Each pod consists of:
-
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.
-
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.
Configuration Reference
Section titled “Configuration Reference”Daemon Core Settings
Section titled “Daemon Core Settings”All daemon settings live under config.daemon:
| Parameter | Description | Default |
|---|---|---|
config.daemon.trustDomain | SPIFFE trust domain (must match control plane) | example.com |
config.daemon.defaultCertTTL | Default TTL for issued workload certificates | 2h |
config.daemon.dataDir | Persistent data directory inside the container | /data/riptides |
config.daemon.kernelModuleDevice | Path to the kernel module character device | /host/dev/riptides |
config.daemon.ephemeral | Auto-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) |
Control Plane Connection
Section titled “Control Plane Connection”Settings under config.daemon.controlPlane:
| Parameter | Description | Default |
|---|---|---|
config.daemon.controlPlane.enabled | Enable control plane connection | true |
config.daemon.controlPlane.url | HTTPS base URL of the control plane | https://controlplane:8443 |
config.daemon.controlPlane.grpcServerAddress | gRPC endpoint (host:port) | none |
config.daemon.controlPlane.tokenBrokerBaseURL | Token broker endpoint URL | none |
config.daemon.controlPlane.allowInsecureConnection | Skip TLS verification (development only) | false |
config.daemon.controlPlane.authPlugin.type | Authentication 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-brokerTunnel Server
Section titled “Tunnel Server”Configure a tunnel connection so the control plane can retrieve telemetry from the daemon in a pull model:
| Parameter | Description | Default |
|---|---|---|
config.daemon.tunnelServer.address | Tunnel server endpoint (host:port) | none |
config: daemon: tunnelServer: address: tunnel.example.com:443Authentication Plugins
Section titled “Authentication Plugins”The daemon authenticates to the control plane using one of these plugins:
Kubernetes Service Account Token (K8sSAT)
Section titled “Kubernetes Service Account Token (K8sSAT)”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/v1alpha1kind: Verifiermetadata: name: k8s-cluster namespace: riptides-systemspec: 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.
Join Token
Section titled “Join Token”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/v1alpha1kind: Verifiermetadata: name: jointoken namespace: riptides-systemspec: joinToken: {}---apiVersion: auth.riptides.io/v1alpha1kind: JoinTokenmetadata: name: my-join-token namespace: riptides-systemspec: token: "your-join-token-here"AWS Instance Identity Document (AWSIID)
Section titled “AWS Instance Identity Document (AWSIID)”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: AWSIIDGCP Instance Identity Token (GCPIIT)
Section titled “GCP Instance Identity Token (GCPIIT)”Recommended for GCP deployments. Uses the GCP instance identity token:
config: daemon: controlPlane: authPlugin: type: GCPIITMetadata Collectors
Section titled “Metadata Collectors”Metadata collectors enrich workload identity information with context from the runtime environment. Configure under config.daemon.metadataCollectors:
| Collector | Description | Default |
|---|---|---|
procfs | Collects process information from /proc | enabled: true |
procfs.extractEnvs | Also collect environment variables | false |
linuxos | Collects OS release information | enabled: true |
sysfsdmi | Collects DMI/SMBIOS hardware info | enabled: true |
ec2 | AWS EC2 instance metadata | enabled: true |
gcp | GCP instance metadata | enabled: true |
azure | Azure instance metadata | enabled: true |
kubernetes | Kubernetes pod and node metadata | enabled: true |
docker | Docker container metadata | enabled: false |
For the Kubernetes metadata collector:
| Parameter | Description | Default |
|---|---|---|
kubernetes.kubeletHost | Kubelet API host | localhost |
kubernetes.kubeletPort | Kubelet API port | 10250 |
kubernetes.kubeletCA | Path to kubelet CA certificate | /etc/kubernetes/pki/ca.crt |
kubernetes.skipKubeletVerification | Skip kubelet TLS verification | true |
kubernetes.credentials | Path 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: falseDriver Loader
Section titled “Driver Loader”The driver loader init container loads the Riptides kernel module before the daemon starts:
| Parameter | Description | Default |
|---|---|---|
driverLoader.image.repository | Driver loader image | ghcr.io/riptides-packages/images/driver-loader |
driverLoader.image.pullPolicy | Pull policy | IfNotPresent |
driverLoader.driverVersion | Driver version (git branch, tag, or commit) | main |
driverLoader.commandTimeout | Timeout in seconds for load commands | 5 |
driverLoader.dynDBG | Enable kernel dynamic debug for the module | false |
Health Check
Section titled “Health Check”| Parameter | Description | Default |
|---|---|---|
healthStatus.port | Daemon health check port | 10100 |
Example: AWS EKS Deployment
Section titled “Example: AWS EKS Deployment”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: ExistsVerifying the Deployment
Section titled “Verifying the Deployment”Check that daemon pods are running on all nodes:
kubectl get pods -n riptides-system -l app.kubernetes.io/name=daemon -o wideCheck the driver loader init container logs to confirm the kernel module loaded:
kubectl logs -n riptides-system -l app.kubernetes.io/name=daemon -c daemon-driver-loaderCheck the daemon logs for control plane connectivity:
kubectl logs -n riptides-system -l app.kubernetes.io/name=daemon -fVerify health:
kubectl port-forward -n riptides-system daemonset/riptides-daemon 10100:10100curl http://localhost:10100/healthzTroubleshooting
Section titled “Troubleshooting”Driver loader fails
Section titled “Driver loader fails”If the init container fails, the kernel module loader may have failed. Check the kernel module loader init container.
kubectl logs -n riptides-system <failed-daemon-pod> -c daemon-driver-loaderDaemon cannot reach control plane
Section titled “Daemon cannot reach control plane”Verify network connectivity from a workload node to the control plane endpoints:
kubectl run -n riptides-system netcheck --rm -it --image=busybox -- \ wget -qO- --timeout=5 https://cp.example.com/healthzEnsure 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.
Authentication failures
Section titled “Authentication failures”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:
az aks safeguards update --resource-group <rg> --name <cluster> \ --level Enforce --excluded-ns riptides-systemEven 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/proxypolicy has no namespace exclusion, so the daemon’snodes/proxyClusterRole 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 themetadataCollectors.kuberneteskubelet settings at their defaults. The chart has no flag to drop this ClusterRole on its own (it is created along withserviceAccount.create), so strip it with a Helm--post-rendererpatch, 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.
Upgrading
Section titled “Upgrading”helm upgrade riptides-daemon oci://ghcr.io/riptides-packages/helm/daemon \ --namespace riptides-system \ -f daemon-values.yamlThe DaemonSet performs a rolling update by default. The driver loader init container will reload the kernel module on each node as pods restart.