Skip to content

Commit 29c905a

Browse files
TomerNewmankubernetes-prow[bot]
authored andcommitted
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.
1 parent 9547ce6 commit 29c905a

2 files changed

Lines changed: 214 additions & 0 deletions

File tree

internal/webhook/module.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"errors"
2222
"fmt"
23+
"path/filepath"
2324
"regexp"
2425
"strconv"
2526
"strings"
@@ -162,6 +163,10 @@ func validateModule(mod *kmmv1beta1.Module, kubeVersion *KubeVersion) (admission
162163
return nil, fmt.Errorf("failed to validate DRA: %v", err)
163164
}
164165

166+
if err := validateDevicePluginVolumes(mod.Spec.DevicePlugin); err != nil {
167+
return nil, fmt.Errorf("failed to validate device plugin volumes: %v", err)
168+
}
169+
165170
if mod.Spec.ModuleLoader == nil {
166171
// If ModuleLoader is nil, there is no need to validate related fields
167172
return nil, nil
@@ -222,6 +227,35 @@ func validateDRA(mod *kmmv1beta1.Module, kubeVersion *KubeVersion) error {
222227
return nil
223228
}
224229

230+
var allowedHostPathPrefixes = []string{"/dev", "/sys", "/var", "/opt"}
231+
232+
func isAllowedHostPath(hostPath string) bool {
233+
p := filepath.Clean(hostPath)
234+
for _, prefix := range allowedHostPathPrefixes {
235+
if p == prefix || strings.HasPrefix(p, prefix+"/") {
236+
return true
237+
}
238+
}
239+
return false
240+
}
241+
242+
func validateDevicePluginVolumes(dp *kmmv1beta1.DevicePluginSpec) error {
243+
if dp == nil {
244+
return nil
245+
}
246+
247+
for i, vol := range dp.Volumes {
248+
if vol.HostPath != nil && !isAllowedHostPath(vol.HostPath.Path) {
249+
return fmt.Errorf(
250+
"spec.devicePlugin.volumes[%d]: hostPath %q is not allowed; only /dev, /sys, /var and /opt paths are permitted",
251+
i, vol.HostPath.Path,
252+
)
253+
}
254+
}
255+
256+
return nil
257+
}
258+
225259
func validateImageFormat(img string) error {
226260

227261
if !strings.Contains(img, ":") && !strings.Contains(img, "@") {

internal/webhook/module_test.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,3 +829,183 @@ var _ = Describe("validateDRA", func() {
829829
Expect(validateDRA(mod, minValidKubeVersion)).NotTo(HaveOccurred())
830830
})
831831
})
832+
833+
var _ = Describe("validateDevicePluginVolumes", func() {
834+
It("should accept nil DevicePlugin", func() {
835+
Expect(validateDevicePluginVolumes(nil)).NotTo(HaveOccurred())
836+
})
837+
838+
It("should accept DevicePlugin with no volumes", func() {
839+
dp := &kmmv1beta1.DevicePluginSpec{
840+
Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"},
841+
}
842+
Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred())
843+
})
844+
845+
It("should accept non-hostPath volumes", func() {
846+
dp := &kmmv1beta1.DevicePluginSpec{
847+
Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"},
848+
Volumes: []v1.Volume{
849+
{
850+
Name: "config",
851+
VolumeSource: v1.VolumeSource{
852+
ConfigMap: &v1.ConfigMapVolumeSource{
853+
LocalObjectReference: v1.LocalObjectReference{Name: "my-cm"},
854+
},
855+
},
856+
},
857+
},
858+
}
859+
Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred())
860+
})
861+
862+
It("should accept hostPath volumes under /dev", func() {
863+
dp := &kmmv1beta1.DevicePluginSpec{
864+
Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"},
865+
Volumes: []v1.Volume{
866+
{
867+
Name: "dev-vfio",
868+
VolumeSource: v1.VolumeSource{
869+
HostPath: &v1.HostPathVolumeSource{Path: "/dev/vfio"},
870+
},
871+
},
872+
},
873+
}
874+
Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred())
875+
})
876+
877+
It("should accept hostPath volumes equal to /dev", func() {
878+
dp := &kmmv1beta1.DevicePluginSpec{
879+
Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"},
880+
Volumes: []v1.Volume{
881+
{
882+
Name: "dev",
883+
VolumeSource: v1.VolumeSource{
884+
HostPath: &v1.HostPathVolumeSource{Path: "/dev"},
885+
},
886+
},
887+
},
888+
}
889+
Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred())
890+
})
891+
892+
It("should accept hostPath volumes under /sys", func() {
893+
dp := &kmmv1beta1.DevicePluginSpec{
894+
Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"},
895+
Volumes: []v1.Volume{
896+
{
897+
Name: "sys-class",
898+
VolumeSource: v1.VolumeSource{
899+
HostPath: &v1.HostPathVolumeSource{Path: "/sys/class/net"},
900+
},
901+
},
902+
},
903+
}
904+
Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred())
905+
})
906+
907+
It("should accept hostPath volumes under /var", func() {
908+
dp := &kmmv1beta1.DevicePluginSpec{
909+
Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"},
910+
Volumes: []v1.Volume{
911+
{
912+
Name: "var-lib",
913+
VolumeSource: v1.VolumeSource{
914+
HostPath: &v1.HostPathVolumeSource{Path: "/var/lib/kubelet/device-plugins"},
915+
},
916+
},
917+
},
918+
}
919+
Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred())
920+
})
921+
922+
It("should accept hostPath volumes under /opt", func() {
923+
dp := &kmmv1beta1.DevicePluginSpec{
924+
Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"},
925+
Volumes: []v1.Volume{
926+
{
927+
Name: "opt-lib",
928+
VolumeSource: v1.VolumeSource{
929+
HostPath: &v1.HostPathVolumeSource{Path: "/opt/lib/firmware"},
930+
},
931+
},
932+
},
933+
}
934+
Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred())
935+
})
936+
937+
It("should reject hostPath volumes outside allowed paths", func() {
938+
dp := &kmmv1beta1.DevicePluginSpec{
939+
Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"},
940+
Volumes: []v1.Volume{
941+
{
942+
Name: "root",
943+
VolumeSource: v1.VolumeSource{
944+
HostPath: &v1.HostPathVolumeSource{Path: "/"},
945+
},
946+
},
947+
},
948+
}
949+
Expect(validateDevicePluginVolumes(dp)).To(MatchError(ContainSubstring("not allowed")))
950+
})
951+
952+
It("should reject hostPath volumes under /etc", func() {
953+
dp := &kmmv1beta1.DevicePluginSpec{
954+
Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"},
955+
Volumes: []v1.Volume{
956+
{
957+
Name: "etc",
958+
VolumeSource: v1.VolumeSource{
959+
HostPath: &v1.HostPathVolumeSource{Path: "/etc/config"},
960+
},
961+
},
962+
},
963+
}
964+
Expect(validateDevicePluginVolumes(dp)).To(MatchError(ContainSubstring("not allowed")))
965+
})
966+
967+
It("should reject hostPath with prefix trick like /devious", func() {
968+
dp := &kmmv1beta1.DevicePluginSpec{
969+
Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"},
970+
Volumes: []v1.Volume{
971+
{
972+
Name: "tricky",
973+
VolumeSource: v1.VolumeSource{
974+
HostPath: &v1.HostPathVolumeSource{Path: "/devious"},
975+
},
976+
},
977+
},
978+
}
979+
Expect(validateDevicePluginVolumes(dp)).To(MatchError(ContainSubstring("not allowed")))
980+
})
981+
982+
It("should reject hostPath with path traversal like /dev/../etc", func() {
983+
dp := &kmmv1beta1.DevicePluginSpec{
984+
Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"},
985+
Volumes: []v1.Volume{
986+
{
987+
Name: "traversal",
988+
VolumeSource: v1.VolumeSource{
989+
HostPath: &v1.HostPathVolumeSource{Path: "/dev/../etc/shadow"},
990+
},
991+
},
992+
},
993+
}
994+
Expect(validateDevicePluginVolumes(dp)).To(MatchError(ContainSubstring("not allowed")))
995+
})
996+
997+
It("should accept hostPath with redundant slashes under /sys", func() {
998+
dp := &kmmv1beta1.DevicePluginSpec{
999+
Container: kmmv1beta1.DevicePluginContainerSpec{Image: "img:tag"},
1000+
Volumes: []v1.Volume{
1001+
{
1002+
Name: "sys-clean",
1003+
VolumeSource: v1.VolumeSource{
1004+
HostPath: &v1.HostPathVolumeSource{Path: "/sys//class//net"},
1005+
},
1006+
},
1007+
},
1008+
}
1009+
Expect(validateDevicePluginVolumes(dp)).NotTo(HaveOccurred())
1010+
})
1011+
})

0 commit comments

Comments
 (0)