Skip to content

Host Hardening

The Riptides kernel module is reachable only by a process holding CAP_SYS_ADMIN — see Kernel Interface. On SELinux hosts an optional package narrows that further, so that only the daemon’s own SELinux domain, and privileged containers, can open its devices through /dev, whatever capabilities a process holds.

Nothing on this page is required. The driver runs without it, and the capability check is independent of SELinux.

riptides-selinux ships a policy module that:

  • gives the daemon its own domain, riptides_t, entered when systemd executes /usr/bin/riptides
  • labels the module’s devices, /dev/riptides as riptides_device_t and /dev/riptides_pro as riptides_pro_device_t
  • grants access to those device types to riptides_t, and to privileged containers, which is how the daemon runs on Kubernetes (below)

With it installed and SELinux enforcing, a root shell on the host cannot open either device through /dev.

It is built per distribution release, because a compiled policy module is tied to the selinux-policy version it was built against.

Terminal window
sudo dnf install riptides-selinux
Terminal window
# the module is loaded
sudo semodule -l | grep riptides
# the devices carry the policy's label (needs the driver loaded)
sudo ls -Z /dev/riptides /dev/riptides_pro
# and the daemon is in its own domain
ps -eZ | grep riptides

The device check is the one that matters, and a working policy makes it fail: root may not even look at a confined device, so Permission denied is the result you want. If ls -Z shows device_t instead, the policy is loaded but confining nothing. That happens when the driver was loaded before the policy was installed and the node never got relabeled; have udev relabel it:

Terminal window
sudo udevadm trigger --action=add /sys/class/riptides/riptides /sys/class/misc/riptides_pro

Root with a device node of its own. SELinux checks the label of the node being opened, not the device behind it. Root can create another node for the same device outside /dev and open the driver through that, as it could also load a kernel module of its own. So the policy stops casual and accidental access by root, not a determined attacker holding root; against that, the control is the module’s capability check and who holds CAP_SYS_ADMIN.

The daemon itself. It reads /proc of every process on the host to attribute connections to workloads, and manages the system trust store, so a policy permitting its real behaviour would be broad enough to carry little value. The policy confines the device, not the daemon.

Kubernetes. The daemon runs there as a privileged container, which does not enter riptides_t, so the policy grants privileged containers access to the device — without that the DaemonSet could not start. On a Kubernetes node the controls are the module’s capability check and who is permitted to schedule a privileged pod. A privileged pod can already load its own kernel module.

There is no AppArmor profile, and this is a considered position rather than an omission.

AppArmor is path-based and only restricts processes that carry a profile, so it cannot express “this device is reachable by the daemon alone” against the unprofiled processes that are the concern. A profile confining the daemon itself would have to permit reading every process’s memory maps and executables, ptrace, and a broad capability set — which is not meaningfully different from running unconfined.

On Debian and Ubuntu hosts the control is therefore the CAP_SYS_ADMIN check in the module. If your policy requires a profile for third-party daemons, contact us and we will supply one, with that caveat stated.

By default the module fails open: if policy evaluation cannot complete, the connection proceeds unprotected rather than breaking. That keeps a control-plane problem from becoming an application outage, but it means a loss of protection is invisible to the application, which still succeeds.

Deployments that prefer the opposite trade-off should set:

Terminal window
echo Y | sudo tee /sys/module/riptides/parameters/eval_fail_closed

Connections are then reset instead, whether the evaluation that failed was the one made when the connection opened or a message’s traffic evaluation (L7 and agent policy). Traffic the daemon receives but cannot parse is still passed through: that is the daemon’s own choice, not a failed evaluation.

It does not cover a daemon that is down. Until the daemon is connected and has pushed its configuration, trust anchors and a workload identity, the module evaluates no new connection at all and lets each one through untouched, with or without eval_fail_closed. That is the state after a daemon crash, a restart, or a write to /sys/class/riptides/force_close, which kills the daemon.

Monitor for it either way: command timeout lines in dmesg mark evaluations the daemon did not answer, and /sys/module/riptides/health should read OK. Alert on anything else: Waiting for daemon, Daemon is connected, but no workload identity defined. and OK (disabled) all mean new connections are not being protected.

Kernel lockdown and Secure Boot have not been tested. The module is currently unsigned, so on a host enforcing module signatures it will not load. If you run either, talk to us before a deployment rather than after.