From 93a10ba5ea7e2f29735dfc0e75836e003a184434 Mon Sep 17 00:00:00 2001 From: TomerNewman Date: Tue, 23 Jun 2026 09:03:42 +0300 Subject: [PATCH] Restrict device plugin hostPath volumes to allowed prefixes Add webhook validation that rejects hostPath volumes in spec.devicePlugin.volumes unless the path resolves under /dev, /sys, /var or /opt. This prevents device plugins from mounting arbitrary host directories such as / or /etc. --- internal/webhook/module.go | 34 ++++++ internal/webhook/module_test.go | 180 ++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) diff --git a/internal/webhook/module.go b/internal/webhook/module.go index 95ca00901..25d1514fe 100644 --- a/internal/webhook/module.go +++ b/internal/webhook/module.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "path/filepath" "regexp" "strconv" "strings" @@ -162,6 +163,10 @@ func validateModule(mod *kmmv1beta1.Module, kubeVersion *KubeVersion) (admission return nil, fmt.Errorf("failed to validate DRA: %v", err) } + if err := validateDevicePluginVolumes(mod.Spec.DevicePlugin); err != nil { + return nil, fmt.Errorf("failed to validate device plugin volumes: %v", err) + } + if mod.Spec.ModuleLoader == nil { // If ModuleLoader is nil, there is no need to validate related fields return nil, nil @@ -222,6 +227,35 @@ func validateDRA(mod *kmmv1beta1.Module, kubeVersion *KubeVersion) error { return nil } +var allowedHostPathPrefixes = []string{"/dev", "/sys", "/var", "/opt"} + +func isAllowedHostPath(hostPath string) bool { + p := filepath.Clean(hostPath) + for _, prefix := range allowedHostPathPrefixes { + if p == prefix || strings.HasPrefix(p, prefix+"/") { + return true + } + } + return false +} + +func validateDevicePluginVolumes(dp *kmmv1beta1.DevicePluginSpec) error { + if dp == nil { + return nil + } + + for i, vol := range dp.Volumes { + if vol.HostPath != nil && !isAllowedHostPath(vol.HostPath.Path) { + return fmt.Errorf( + "spec.devicePlugin.volumes[%d]: hostPath %q is not allowed; only /dev, /sys, /var and /opt paths are permitted", + i, vol.HostPath.Path, + ) + } + } + + return nil +} + func validateImageFormat(img string) error { if !strings.Contains(img, ":") && !strings.Contains(img, "@") { diff --git a/internal/webhook/module_test.go b/internal/webhook/module_test.go index 57048804b..40c96d5b9 100644 --- a/internal/webhook/module_test.go +++ b/internal/webhook/module_test.go @@ -829,3 +829,183 @@ var _ = Describe("validateDRA", func() { Expect(validateDRA(mod, minValidKubeVersion)).NotTo(HaveOccurred()) }) }) + +var _ = Describe("validateDevicePluginVolumes", func() { + It("should accept nil DevicePlugin", func() { + Expect(validateDevicePluginVolumes(nil)).NotTo(HaveOccurred()) + }) + + It("should accept DevicePlugin with no volumes", func() { + dp := &kmmv1beta1.DevicePluginSpec{ + Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"}, + } + Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred()) + }) + + It("should accept non-hostPath volumes", func() { + dp := &kmmv1beta1.DevicePluginSpec{ + Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"}, + Volumes: []v1.Volume{ + { + Name: "config", + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{Name: "my-cm"}, + }, + }, + }, + }, + } + Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred()) + }) + + It("should accept hostPath volumes under /dev", func() { + dp := &kmmv1beta1.DevicePluginSpec{ + Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"}, + Volumes: []v1.Volume{ + { + Name: "dev-vfio", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{Path: "/dev/vfio"}, + }, + }, + }, + } + Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred()) + }) + + It("should accept hostPath volumes equal to /dev", func() { + dp := &kmmv1beta1.DevicePluginSpec{ + Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"}, + Volumes: []v1.Volume{ + { + Name: "dev", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{Path: "/dev"}, + }, + }, + }, + } + Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred()) + }) + + It("should accept hostPath volumes under /sys", func() { + dp := &kmmv1beta1.DevicePluginSpec{ + Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"}, + Volumes: []v1.Volume{ + { + Name: "sys-class", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{Path: "/sys/class/net"}, + }, + }, + }, + } + Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred()) + }) + + It("should accept hostPath volumes under /var", func() { + dp := &kmmv1beta1.DevicePluginSpec{ + Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"}, + Volumes: []v1.Volume{ + { + Name: "var-lib", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{Path: "/var/lib/kubelet/device-plugins"}, + }, + }, + }, + } + Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred()) + }) + + It("should accept hostPath volumes under /opt", func() { + dp := &kmmv1beta1.DevicePluginSpec{ + Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"}, + Volumes: []v1.Volume{ + { + Name: "opt-lib", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{Path: "/opt/lib/firmware"}, + }, + }, + }, + } + Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred()) + }) + + It("should reject hostPath volumes outside allowed paths", func() { + dp := &kmmv1beta1.DevicePluginSpec{ + Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"}, + Volumes: []v1.Volume{ + { + Name: "root", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{Path: "/"}, + }, + }, + }, + } + Expect(validateDevicePluginVolumes(dp)).To(MatchError(ContainSubstring("not allowed"))) + }) + + It("should reject hostPath volumes under /etc", func() { + dp := &kmmv1beta1.DevicePluginSpec{ + Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"}, + Volumes: []v1.Volume{ + { + Name: "etc", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{Path: "/etc/config"}, + }, + }, + }, + } + Expect(validateDevicePluginVolumes(dp)).To(MatchError(ContainSubstring("not allowed"))) + }) + + It("should reject hostPath with prefix trick like /devious", func() { + dp := &kmmv1beta1.DevicePluginSpec{ + Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"}, + Volumes: []v1.Volume{ + { + Name: "tricky", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{Path: "/devious"}, + }, + }, + }, + } + Expect(validateDevicePluginVolumes(dp)).To(MatchError(ContainSubstring("not allowed"))) + }) + + It("should reject hostPath with path traversal like /dev/../etc", func() { + dp := &kmmv1beta1.DevicePluginSpec{ + Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"}, + Volumes: []v1.Volume{ + { + Name: "traversal", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{Path: "/dev/../etc/shadow"}, + }, + }, + }, + } + Expect(validateDevicePluginVolumes(dp)).To(MatchError(ContainSubstring("not allowed"))) + }) + + It("should accept hostPath with redundant slashes under /sys", func() { + dp := &kmmv1beta1.DevicePluginSpec{ + Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"}, + Volumes: []v1.Volume{ + { + Name: "sys-clean", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{Path: "/sys//class//net"}, + }, + }, + }, + } + Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred()) + }) +})