
Fun Stuff — Controlling Case LEDs from Kubernetes
Every serious infrastructure project needs a completely unnecessary feature. This is ours: controlling the ARGB LED fans on gpu-1 from a Kubernetes DaemonSet, managed by ArgoCD, triggered by a git push. GitOps for RGB. Because we have standards.
The Hardware
The gpu-1 node lives in a FOIFKIN F1 case, which ships with six pre-installed PWM ARGB fans connected through an internal hub. The hub has a button on it, but that button does nothing useful in a headless rack environment. The fans light up on boot in whatever rainbow pattern the hub feels like, and they stay that way forever unless you take software control.
The motherboard is a Gigabyte Z790 Eagle AX. Buried on it is an ITE IT5701 USB RGB controller (vendor 048D, product 5702) that manages the motherboard’s addressable LED headers — and therefore the fans connected through the hub. This controller exposes itself as a USB HID device at /dev/hidraw0, which turns out to be the key detail.
USB HID vs I2C: A Brief Detour
The original plan was to use the I2C/SMBus path. Gigabyte boards typically expose their RGB controllers on the SMBus, and OpenRGB supports that route well. The plan called for loading i2c-dev and i2c-i801 kernel modules via a Talos machine config patch, adding acpi_enforce_resources=lax as a kernel argument (Gigabyte boards need this), and probing the I2C bus.
That plan lasted about ten minutes. Talos Linux does not compile CONFIG_I2C_CHARDEV into the kernel, so i2c-dev simply cannot load. No /dev/i2c-* devices, no SMBus access, end of story.
Fortunately, while checking dmesg output for I2C clues, the USB HID device was staring right back:
hid-generic 0003:048D:5702.0001: hidraw0: USB HID v1.12 Device [ITE Tech. Inc. ITE Device]Talos ships with CONFIG_HIDRAW=y and CONFIG_USB_HID=y built-in. No kernel modules to load, no Talos patches required. The device is just there, at /dev/hidraw0, ready to be poked by OpenRGB. The USB HID path is also safer than I2C — no risk of accidentally probing dangerous SMBus addresses.
Discovery
Before writing any manifests, we needed to know what OpenRGB would actually detect. A one-shot discovery pod on gpu-1 did the trick:
kubectl run openrgb-discovery --rm -it --restart=Never \
--image=swensorm/openrgb:release_0.9 \
--overrides='{
"spec": {
"nodeSelector": {"kubernetes.io/hostname": "gpu-1"},
"tolerations": [{"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}],
"containers": [{
"name": "openrgb-discovery",
"image": "swensorm/openrgb:release_0.9",
"command": ["/usr/app/openrgb", "--list-devices"],
"securityContext": {"privileged": true},
"volumeMounts": [{"name": "dev", "mountPath": "/dev"}]
}],
"volumes": [{"name": "dev", "hostPath": {"path": "/dev"}}]
}
}' -- /usr/app/openrgb --list-devicesThis revealed one device — Z790 EAGLE AX (IT5701-GIGABYTE) at device index 0 — with three zones (D_LED1 Bottom, D_LED2 Top, Motherboard), eight LEDs total, and a handful of modes: Direct, Static, Breathing, Blinking, Color Cycle, and Flashing. Enough to work with.
The OpenRGB DaemonSet
The deployment is a DaemonSet pinned to gpu-1. The architecture is a single container: it
runs openrgb --noautoconnect $OPENRGB_ARGS to apply the LED config at startup, then
sleep infinity to keep the pod alive.
The --noautoconnect flag is the key detail. It runs OpenRGB in standalone mode without
starting a local server. This matters because of a subtle hardware behavior: the IT5701
controller saves its last color to non-volatile memory, and when OpenRGB starts a server, the
server’s device initialization sequence restores that saved state — overwriting whatever the
config just applied. Standalone mode applies the config and exits cleanly, with nothing to
undo it afterward.
The pod runs privileged with /dev mounted from the host, giving it access to the HID device
(currently at /dev/hidraw2 — the device path can shift after hardware changes, but OpenRGB
uses device index -d 0, not the hidraw path directly). The gpu-1 node carries a
nvidia.com/gpu=present:NoSchedule taint, so the DaemonSet needs a matching toleration.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: openrgb
namespace: openrgb
spec:
selector:
matchLabels:
app: openrgb
template:
spec:
nodeSelector:
kubernetes.io/hostname: gpu-1
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- name: openrgb
image: ghcr.io/derio-net/openrgb:1.0rc2
command: ["/bin/sh", "-c"]
args:
- |
sleep 5
/usr/app/openrgb --noautoconnect $OPENRGB_ARGS
sleep infinity
env:
- name: OPENRGB_ARGS
valueFrom:
configMapKeyRef:
name: openrgb-config
key: OPENRGB_ARGS
securityContext:
privileged: true
volumeMounts:
- name: dev
mountPath: /dev
resources:
requests:
memory: "32Mi"
cpu: "10m"
limits:
memory: "128Mi"
volumes:
- name: dev
hostPath:
path: /devA note on the image: no pre-built container exists for OpenRGB 1.0rc2. We build our own
from the Codeberg source (release_candidate_1.0rc2 tag) via a GitHub Actions workflow
that pushes to ghcr.io/derio-net/openrgb:1.0rc2. The binary lands at
/usr/app/openrgb, same path as the old swensorm/openrgb image.
The Server Detour
The original implementation used a two-container design: an init container to apply the LED
config, and a main container running openrgb --server as a keepalive. It appeared to work.
It stopped appearing to work during an unrelated hardware session — reseating the RTX 5070, resetting the CMOS battery, and rebooting the node several times. The LEDs came back as green, then blue, then lila. Each reboot a different color. The server was the culprit: it reinitializes the device every time the pod starts, and initialization restores the controller’s non-volatile saved state from the last write — which varied depending on which OpenRGB invocation had touched it most recently.
The fix is the design above. The server was added speculatively as a keepalive and never used
as a remote control interface. sleep infinity does the same job without touching the device.
ConfigMap-Driven LED Config
The entire LED configuration lives in a single ConfigMap value — the CLI arguments passed to OpenRGB:
apiVersion: v1
kind: ConfigMap
metadata:
name: openrgb-config
namespace: openrgb
data:
# Device 0: Z790 EAGLE AX (IT5701-GIGABYTE V3.5.14.0) at /dev/hidraw2
# Zones: D_LED1, D_LED2 (ARGB strips)
# NOTE: On-demand writes currently blocked — see "The Firmware Detour" below
OPENRGB_ARGS: "-d 0 -m Static -c 000000"The -d 0 selects the device, -m Static sets the mode, and -c 000000 sets the color — in this case, black (LEDs off). The workflow to change the LED color on a live cluster:
- Edit the
OPENRGB_ARGSvalue inapps/openrgb/manifests/configmap.yaml - Commit and push
- ArgoCD detects the change and syncs
- The DaemonSet pod restarts, the container applies the new config on startup
- The fans… stay exactly as they are
That is a five-stage pipeline that terminates in nothing. We will get to why shortly.
The Firmware Detour
After getting the DaemonSet running and confirming the pod was Synced/Healthy in ArgoCD, we noticed the fans were still showing a rainbow pattern from the last cold boot. OpenRGB reported success. The HID device accepted every write. The LEDs ignored all of it.
The investigation went deep. A privileged pod ran Python directly against /dev/hidraw2 using the correct HIDIOCSFEATURE ioctl (0xC0404806). Readback via HIDIOCGFEATURE confirmed the device was storing the writes — register state changed exactly as expected. 245 rapid write cycles. Zero effect on the physical LEDs.
The culprit is a BIOS update. The Z790 Eagle AX shipped with IT5701 firmware that OpenRGB supported. Somewhere between the original installation and now, a BIOS update (F3 → F6) swapped in IT5701 firmware version V3.5.14.0. This firmware version requires an unlock handshake before it will apply LED writes — a handshake that is not in OpenRGB 0.9 or 1.0rc2 for this specific PID (0x5702).
The device is not broken. It is not a permissions problem (udev rules were verified). It is not USB autosuspend. The IT5701 simply will not apply color writes until something sends the right initialization sequence, and that sequence is not public. The only reliable way to reverse-engineer it is to capture USB traffic from the Windows RGB Fusion application while it successfully changes the color.
KubeVirt is already on the roadmap. When it arrives, a Windows VM with USB passthrough for 048d:5702 will let us capture that traffic and finally close this loop. For now, the fans are rainbow, the pod is Synced/Healthy, and the ConfigMap is twelve lines of aspiration.
ArgoCD Integration
The ArgoCD side follows the same plain-manifests pattern used by longhorn-extras — no Helm chart, just a directory of YAML files. The Application template in the root App-of-Apps points at apps/openrgb/manifests:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: openrgb
namespace: argocd
spec:
project: infrastructure
source:
repoURL: {{ .Values.repoURL }}
targetRevision: {{ .Values.targetRevision }}
path: apps/openrgb/manifests
destination:
server: {{ .Values.destination.server }}
namespace: openrgb
syncPolicy:
automated:
prune: false
selfHeal: trueSelf-heal is enabled, so if someone manually messes with the LED config on the node, ArgoCD will restore the DaemonSet to the declared state. Your LED colors are now protected by GitOps. You are welcome.
Was It Worth It?
Let us take stock. To control six case fans, we: ruled out I2C, ran a discovery pod, wrote a DaemonSet and a ConfigMap, registered an ArgoCD Application, set up a namespace with Pod Security Admission labels, built a custom OpenRGB container image with a GitHub Actions pipeline, applied Talos udev rules via Omni machine config, ran 245 HID feature report writes with correct ioctls and verified register state, and reverse-engineered enough of the IT5701 protocol to know exactly why it does not work.
The fans are rainbow. They were rainbow when we started. They are rainbow now.
The pod requests 10 millicores of CPU and 32Mi of memory. It is Synced/Healthy in ArgoCD. It runs sleep infinity approximately full-time. The LEDs ignore it completely.
Absolutely not worth it. But we now know more about HID feature reports, IT5701 firmware versioning, and Talos udev rule syntax than any reasonable person should. And when KubeVirt arrives and we finally capture that Windows USB traffic, we will have the best-documented RGB setup in any homelab that has never successfully changed an LED color on demand.
References
- OpenRGB — Open-source RGB lighting control across manufacturers
- OpenRGB GitLab Repository — Source code and device compatibility information
- OpenRGB Supported Devices Wiki — Device detection, compatibility, and protocol documentation
- Linux HID Subsystem — Kernel documentation for USB HID and hidraw devices
- Kubernetes DaemonSet — Official DaemonSet documentation for node-level workloads
- IT5701 Investigation Notes — Full analysis of the firmware write lock, ioctl findings, and recommended fix path