selinux: add a new SELinux translator to the controller

A real SELinuxOptionsToFileLabel function needs access to host's
/etc/selinux to read the defaults. This is not possible in
kube-controller-manager that often runs in a container and does not have
access to /etc on the host. Even if it had, it could run on a different
Linux distro than worker nodes.

Therefore implement a custom SELinuxOptionsToFileLabel that does not
default fields in SELinuxOptions and uses just fields provided by the Pod.

Since the controller cannot default empty SELinux label components,
treat them as incomparable.
Example: "system_u:system_r:container_t:s0:c1,c2" *does not* conflict with ":::s0:c1,c2",
because the node that will run such a Pod may expand "":::s0:c1,c2" to "system_u:system_r:container_t:s0:c1,c2".
However, "system_u:system_r:container_t:s0:c1,c2" *does* conflict with ":::s0:c98,c99".
This commit is contained in:
Jan Safranek 2025-02-11 10:18:07 +01:00
parent 20b12ad5c3
commit 2050d6fc69
6 changed files with 407 additions and 65 deletions

View File

@ -23,6 +23,7 @@ import (
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/controller/volume/selinuxwarning/translator"
)
const (
@ -51,7 +52,8 @@ type VolumeCache interface {
// VolumeCache stores all volumes used by Pods and their properties that the controller needs to track,
// like SELinux labels and SELinuxChangePolicies.
type volumeCache struct {
mutex sync.RWMutex
mutex sync.RWMutex
seLinuxTranslator *translator.ControllerSELinuxTranslator
// All volumes of all existing Pods.
volumes map[v1.UniqueVolumeName]usedVolume
}
@ -59,9 +61,10 @@ type volumeCache struct {
var _ VolumeCache = &volumeCache{}
// NewVolumeLabelCache creates a new VolumeCache.
func NewVolumeLabelCache() VolumeCache {
func NewVolumeLabelCache(seLinuxTranslator *translator.ControllerSELinuxTranslator) VolumeCache {
return &volumeCache{
volumes: make(map[v1.UniqueVolumeName]usedVolume),
seLinuxTranslator: seLinuxTranslator,
volumes: make(map[v1.UniqueVolumeName]usedVolume),
}
}
@ -137,7 +140,7 @@ func (c *volumeCache) AddVolume(logger klog.Logger, volumeName v1.UniqueVolumeNa
OtherPropertyValue: string(changePolicy),
})
}
if otherPodInfo.seLinuxLabel != label {
if c.seLinuxTranslator.Conflicts(otherPodInfo.seLinuxLabel, label) {
// Send conflict to both pods
conflicts = append(conflicts, Conflict{
PropertyName: "SELinuxLabel",
@ -248,7 +251,7 @@ func (c *volumeCache) SendConflicts(logger klog.Logger, ch chan<- Conflict) {
OtherPropertyValue: string(otherPodInfo.changePolicy),
}
}
if podInfo.seLinuxLabel != otherPodInfo.seLinuxLabel {
if c.seLinuxTranslator.Conflicts(podInfo.seLinuxLabel, otherPodInfo.seLinuxLabel) {
ch <- Conflict{
PropertyName: "SELinuxLabel",
EventReason: "SELinuxLabelConflict",

View File

@ -25,6 +25,7 @@ import (
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
"k8s.io/klog/v2/ktesting"
"k8s.io/kubernetes/pkg/controller/volume/selinuxwarning/translator"
)
func getTestLoggers(t *testing.T) (klog.Logger, klog.Logger) {
@ -47,7 +48,8 @@ func sortConflicts(conflicts []Conflict) {
// Delete all items in a bigger cache and check it's empty
func TestVolumeCache_DeleteAll(t *testing.T) {
var podsToDelete []cache.ObjectName
c := NewVolumeLabelCache().(*volumeCache)
seLinuxTranslator := &translator.ControllerSELinuxTranslator{}
c := NewVolumeLabelCache(seLinuxTranslator).(*volumeCache)
logger, dumpLogger := getTestLoggers(t)
// Arrange: add a lot of volumes to the cache
@ -110,42 +112,70 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
podNamespace: "ns1",
podName: "pod1-mountOption",
volumeName: "vol1",
label: "label1",
label: "system_u:system_r:label1",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
{
podNamespace: "ns2",
podName: "pod2-recursive",
volumeName: "vol2",
label: "label2",
label: "system_u:system_r:label2",
changePolicy: v1.SELinuxChangePolicyRecursive,
},
{
podNamespace: "ns3",
podName: "pod3-1",
volumeName: "vol3", // vol3 is used by 2 pods with the same label + recursive policy
label: "label3",
label: "system_u:system_r:label3",
changePolicy: v1.SELinuxChangePolicyRecursive,
},
{
podNamespace: "ns3",
podName: "pod3-2",
volumeName: "vol3", // vol3 is used by 2 pods with the same label + recursive policy
label: "label3",
label: "system_u:system_r:label3",
changePolicy: v1.SELinuxChangePolicyRecursive,
},
{
podNamespace: "ns4",
podName: "pod4-1",
volumeName: "vol4", // vol4 is used by 2 pods with the same label + mount policy
label: "label4",
label: "system_u:system_r:label4",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
{
podNamespace: "ns4",
podName: "pod4-2",
volumeName: "vol4", // vol4 is used by 2 pods with the same label + mount policy
label: "label4",
label: "system_u:system_r:label4",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
{
podNamespace: "ns5",
podName: "pod5",
volumeName: "vol5", // vol5 has no user and role
label: "::label5",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
{
podNamespace: "ns6",
podName: "pod6",
volumeName: "vol6", // vol6 has no user
label: ":system_r:label6",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
{
podNamespace: "ns7",
podName: "pod7",
volumeName: "vol7", // vol7 has no user and role, but has categories
label: "::label7:c0,c1",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
{
podNamespace: "ns8",
podName: "pod8",
volumeName: "vol8", // vol has no label
label: "",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
}
@ -163,7 +193,7 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
podNamespace: "testns",
podName: "testpod",
volumeName: "vol-new",
label: "label-new",
label: "system_u:system_r:label-new",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
expectedConflicts: nil,
@ -175,7 +205,7 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
podNamespace: "testns",
podName: "testpod",
volumeName: "vol-new",
label: "label-new",
label: "system_u:system_r:label-new",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
expectedConflicts: nil,
@ -187,7 +217,7 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
podNamespace: "testns",
podName: "testpod",
volumeName: "vol1",
label: "label1",
label: "system_u:system_r:label1",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
expectedConflicts: nil,
@ -199,7 +229,7 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
podNamespace: "testns",
podName: "testpod",
volumeName: "vol1",
label: "label-new",
label: "system_u:system_r:label-new",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
expectedConflicts: []Conflict{
@ -207,9 +237,9 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
PropertyName: "SELinuxLabel",
EventReason: "SELinuxLabelConflict",
Pod: cache.ObjectName{Namespace: "testns", Name: "testpod"},
PropertyValue: "label-new",
PropertyValue: "system_u:system_r:label-new",
OtherPod: cache.ObjectName{Namespace: "ns1", Name: "pod1-mountOption"},
OtherPropertyValue: "label1",
OtherPropertyValue: "system_u:system_r:label1",
},
},
},
@ -220,7 +250,7 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
podNamespace: "testns",
podName: "testpod",
volumeName: "vol1",
label: "label1",
label: "system_u:system_r:label1",
changePolicy: v1.SELinuxChangePolicyRecursive,
},
expectedConflicts: []Conflict{
@ -241,7 +271,7 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
podNamespace: "testns",
podName: "testpod",
volumeName: "vol1",
label: "label-new",
label: "system_u:system_r:label-new",
changePolicy: v1.SELinuxChangePolicyRecursive,
},
expectedConflicts: []Conflict{
@ -257,9 +287,9 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
PropertyName: "SELinuxLabel",
EventReason: "SELinuxLabelConflict",
Pod: cache.ObjectName{Namespace: "testns", Name: "testpod"},
PropertyValue: "label-new",
PropertyValue: "system_u:system_r:label-new",
OtherPod: cache.ObjectName{Namespace: "ns1", Name: "pod1-mountOption"},
OtherPropertyValue: "label1",
OtherPropertyValue: "system_u:system_r:label1",
},
},
},
@ -271,7 +301,7 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
podNamespace: "ns2",
podName: "pod2-recursive",
volumeName: "vol2", // there is no other pod that uses vol2 -> change of policy and label is possible
label: "label-new", // was label2 in the original pod2
label: "system_u:system_r:label-new", // was label2 in the original pod2
changePolicy: v1.SELinuxChangePolicyMountOption, // was Recursive in the original pod2
},
expectedConflicts: nil,
@ -284,7 +314,7 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
podNamespace: "ns3",
podName: "pod3-1",
volumeName: "vol3", // vol3 is used by pod3-2 with label3 and Recursive policy
label: "label-new", // Technically, it's not possible to change a label of an existing pod, but we still check for conflicts
label: "system_u:system_r:label-new", // Technically, it's not possible to change a label of an existing pod, but we still check for conflicts
changePolicy: v1.SELinuxChangePolicyMountOption, // ChangePolicy change can happen when CSIDriver is updated from SELinuxMount: false to SELinuxMount: true
},
expectedConflicts: []Conflict{
@ -300,18 +330,88 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
PropertyName: "SELinuxLabel",
EventReason: "SELinuxLabelConflict",
Pod: cache.ObjectName{Namespace: "ns3", Name: "pod3-1"},
PropertyValue: "label-new",
PropertyValue: "system_u:system_r:label-new",
OtherPod: cache.ObjectName{Namespace: "ns3", Name: "pod3-2"},
OtherPropertyValue: "label3",
OtherPropertyValue: "system_u:system_r:label3",
},
},
},
{
name: "existing volume in a new pod with existing policy and new incomparable label (missing user and role)",
initialPods: existingPods,
podToAdd: podWithVolume{
podNamespace: "testns",
podName: "testpod",
volumeName: "vol5",
label: "system_u:system_r:label5",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
expectedConflicts: []Conflict{},
},
{
name: "existing volume in a new pod with conflicting policy with incomparable parts",
initialPods: existingPods,
podToAdd: podWithVolume{
podNamespace: "testns",
podName: "testpod",
volumeName: "vol5",
label: "::label6",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
expectedConflicts: []Conflict{
{
PropertyName: "SELinuxLabel",
EventReason: "SELinuxLabelConflict",
Pod: cache.ObjectName{Namespace: "testns", Name: "testpod"},
PropertyValue: "::label6",
OtherPod: cache.ObjectName{Namespace: "ns5", Name: "pod5"},
OtherPropertyValue: "::label5",
},
},
},
{
name: "existing volume in a new pod with existing policy and new incomparable label (missing user)",
initialPods: existingPods,
podToAdd: podWithVolume{
podNamespace: "testns",
podName: "testpod",
volumeName: "vol6",
label: "system_u::label6",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
expectedConflicts: []Conflict{},
},
{
name: "existing volume in a new pod with existing policy and new incomparable label (missing categories)",
initialPods: existingPods,
podToAdd: podWithVolume{
podNamespace: "testns",
podName: "testpod",
volumeName: "vol7",
label: "system_u:system_r:label7",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
expectedConflicts: []Conflict{},
},
{
name: "existing volume in a new pod with existing policy and new incomparable label (missing everything)",
initialPods: existingPods,
podToAdd: podWithVolume{
podNamespace: "testns",
podName: "testpod",
volumeName: "vol8",
label: "system_u:system_r:label8",
changePolicy: v1.SELinuxChangePolicyMountOption,
},
expectedConflicts: []Conflict{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger, dumpLogger := getTestLoggers(t)
// Arrange: add initial pods to the cache
c := NewVolumeLabelCache().(*volumeCache)
seLinuxTranslator := &translator.ControllerSELinuxTranslator{}
c := NewVolumeLabelCache(seLinuxTranslator).(*volumeCache)
for _, podToAdd := range tt.initialPods {
conflicts := c.AddVolume(logger, podToAdd.volumeName, cache.ObjectName{Namespace: podToAdd.podNamespace, Name: podToAdd.podName}, podToAdd.label, podToAdd.changePolicy, "csiDriver1")
if len(conflicts) != 0 {
@ -328,6 +428,7 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
sortConflicts(expectedConflicts)
if !reflect.DeepEqual(conflicts, expectedConflicts) {
t.Errorf("AddVolume returned unexpected conflicts: %+v", conflicts)
t.Logf("Expected conflicts: %+v", expectedConflicts)
c.dump(dumpLogger)
}
// Expect the pod + volume to be present in the cache
@ -370,7 +471,8 @@ func TestVolumeCache_AddVolumeSendConflicts(t *testing.T) {
}
func TestVolumeCache_GetPodsForCSIDriver(t *testing.T) {
c := NewVolumeLabelCache().(*volumeCache)
seLinuxTranslator := &translator.ControllerSELinuxTranslator{}
c := NewVolumeLabelCache(seLinuxTranslator).(*volumeCache)
logger, dumpLogger := getTestLoggers(t)
existingPods := map[string][]podWithVolume{

View File

@ -44,6 +44,7 @@ import (
"k8s.io/kubernetes/pkg/controller/volume/attachdetach/util"
"k8s.io/kubernetes/pkg/controller/volume/common"
volumecache "k8s.io/kubernetes/pkg/controller/volume/selinuxwarning/cache"
"k8s.io/kubernetes/pkg/controller/volume/selinuxwarning/translator"
"k8s.io/kubernetes/pkg/volume"
"k8s.io/kubernetes/pkg/volume/csi"
"k8s.io/kubernetes/pkg/volume/csimigration"
@ -74,7 +75,7 @@ type Controller struct {
vpm *volume.VolumePluginMgr
cmpm csimigration.PluginManager
csiTranslator csimigration.InTreeToCSITranslator
seLinuxTranslator volumeutil.SELinuxLabelTranslator
seLinuxTranslator *translator.ControllerSELinuxTranslator
eventBroadcaster record.EventBroadcaster
eventRecorder record.EventRecorder
queue workqueue.TypedRateLimitingInterface[cache.ObjectName]
@ -95,6 +96,8 @@ func NewController(
eventBroadcaster := record.NewBroadcaster(record.WithContext(ctx))
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "selinux_warning"})
seLinuxTranslator := &translator.ControllerSELinuxTranslator{}
c := &Controller{
kubeClient: kubeClient,
podLister: podInformer.Lister(),
@ -107,7 +110,7 @@ func NewController(
csiDriverLister: csiDriverInformer.Lister(),
csiDriversSynced: csiDriverInformer.Informer().HasSynced,
vpm: &volume.VolumePluginMgr{},
seLinuxTranslator: volumeutil.NewSELinuxLabelTranslator(),
seLinuxTranslator: seLinuxTranslator,
eventBroadcaster: eventBroadcaster,
eventRecorder: recorder,
@ -117,7 +120,7 @@ func NewController(
Name: "selinux_warning",
},
),
labelCache: volumecache.NewVolumeLabelCache(),
labelCache: volumecache.NewVolumeLabelCache(seLinuxTranslator),
}
err := c.vpm.InitPlugins(plugins, prober, c)

View File

@ -35,7 +35,6 @@ import (
volumecache "k8s.io/kubernetes/pkg/controller/volume/selinuxwarning/cache"
"k8s.io/kubernetes/pkg/volume"
volumetesting "k8s.io/kubernetes/pkg/volume/testing"
"k8s.io/kubernetes/pkg/volume/util"
"k8s.io/utils/ptr"
)
@ -62,7 +61,7 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
{
name: "existing pod with no volumes",
existingPods: []*v1.Pod{
pod("pod1", "label1", nil),
pod("pod1", "s0:c1,c2", nil),
},
pod: cache.ObjectName{Namespace: namespace, Name: "pod1"},
expectedEvents: nil,
@ -71,7 +70,7 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
{
name: "existing pod with unbound PVC",
existingPods: []*v1.Pod{
podWithPVC("pod1", "label1", nil, "non-existing-pvc", "vol1"),
podWithPVC("pod1", "s0:c1,c2", nil, "non-existing-pvc", "vol1"),
},
pod: cache.ObjectName{Namespace: namespace, Name: "pod1"},
expectError: true, // PVC is missing, add back to queue with exp. backoff
@ -87,7 +86,7 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
pvBoundToPVC("pv1", "pvc1"),
},
existingPods: []*v1.Pod{
podWithPVC("pod1", "label1", nil, "pvc1", "vol1"),
podWithPVC("pod1", "s0:c1,c2", nil, "pvc1", "vol1"),
},
pod: cache.ObjectName{Namespace: namespace, Name: "pod1"},
expectedEvents: nil,
@ -95,7 +94,7 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
{
volumeName: "fake-plugin/pv1",
podKey: cache.ObjectName{Namespace: namespace, Name: "pod1"},
label: "system_u:object_r:container_file_t:label1",
label: ":::s0:c1,c2",
changePolicy: v1.SELinuxChangePolicyMountOption,
csiDriver: "ebs.csi.aws.com", // The PV is a fake EBS volume
},
@ -110,7 +109,7 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
pvBoundToPVC("pv1", "pvc1"),
},
existingPods: []*v1.Pod{
podWithPVC("pod1", "label1", ptr.To(v1.SELinuxChangePolicyRecursive), "pvc1", "vol1"),
podWithPVC("pod1", "s0:c1,c2", ptr.To(v1.SELinuxChangePolicyRecursive), "pvc1", "vol1"),
},
pod: cache.ObjectName{Namespace: namespace, Name: "pod1"},
expectedEvents: nil,
@ -118,7 +117,7 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
{
volumeName: "fake-plugin/pv1",
podKey: cache.ObjectName{Namespace: namespace, Name: "pod1"},
label: "system_u:object_r:container_file_t:label1",
label: ":::s0:c1,c2",
changePolicy: v1.SELinuxChangePolicyRecursive,
csiDriver: "ebs.csi.aws.com", // The PV is a fake EBS volume
},
@ -133,7 +132,7 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
pvBoundToPVC("pv1", "pvc1"),
},
existingPods: []*v1.Pod{
addInlineVolume(pod("pod1", "label1", nil)),
addInlineVolume(pod("pod1", "s0:c1,c2", nil)),
},
pod: cache.ObjectName{Namespace: namespace, Name: "pod1"},
expectedEvents: nil,
@ -141,7 +140,7 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
{
volumeName: "fake-plugin/ebs.csi.aws.com-inlinevol1",
podKey: cache.ObjectName{Namespace: namespace, Name: "pod1"},
label: "system_u:object_r:container_file_t:label1",
label: ":::s0:c1,c2",
changePolicy: v1.SELinuxChangePolicyMountOption,
csiDriver: "ebs.csi.aws.com", // The inline volume is AWS EBS
},
@ -156,7 +155,7 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
pvBoundToPVC("pv1", "pvc1"),
},
existingPods: []*v1.Pod{
addInlineVolume(podWithPVC("pod1", "label1", nil, "pvc1", "vol1")),
addInlineVolume(podWithPVC("pod1", "s0:c1,c2", nil, "pvc1", "vol1")),
},
pod: cache.ObjectName{Namespace: namespace, Name: "pod1"},
expectedEvents: nil,
@ -164,14 +163,14 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
{
volumeName: "fake-plugin/pv1",
podKey: cache.ObjectName{Namespace: namespace, Name: "pod1"},
label: "system_u:object_r:container_file_t:label1",
label: ":::s0:c1,c2",
changePolicy: v1.SELinuxChangePolicyMountOption,
csiDriver: "ebs.csi.aws.com", // The PV is a fake EBS volume
},
{
volumeName: "fake-plugin/ebs.csi.aws.com-inlinevol1",
podKey: cache.ObjectName{Namespace: namespace, Name: "pod1"},
label: "system_u:object_r:container_file_t:label1",
label: ":::s0:c1,c2",
changePolicy: v1.SELinuxChangePolicyMountOption,
csiDriver: "ebs.csi.aws.com", // The inline volume is AWS EBS
},
@ -186,8 +185,8 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
pvBoundToPVC("pv1", "pvc1"),
},
existingPods: []*v1.Pod{
podWithPVC("pod1", "label1", nil, "pvc1", "vol1"),
pod("pod2", "label2", nil),
podWithPVC("pod1", "s0:c1,c2", nil, "pvc1", "vol1"),
pod("pod2", "s0:c98,c99", nil),
},
pod: cache.ObjectName{Namespace: namespace, Name: "pod1"},
conflicts: []volumecache.Conflict{
@ -195,31 +194,31 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
PropertyName: "SELinuxLabel",
EventReason: "SELinuxLabelConflict",
Pod: cache.ObjectName{Namespace: namespace, Name: "pod1"},
PropertyValue: "label1",
PropertyValue: ":::s0:c1,c2",
OtherPod: cache.ObjectName{Namespace: namespace, Name: "pod2"},
OtherPropertyValue: "label2",
OtherPropertyValue: ":::s0:c98,c99",
},
{
PropertyName: "SELinuxLabel",
EventReason: "SELinuxLabelConflict",
Pod: cache.ObjectName{Namespace: namespace, Name: "pod2"},
PropertyValue: "label2",
PropertyValue: ":::s0:c98,c99",
OtherPod: cache.ObjectName{Namespace: namespace, Name: "pod1"},
OtherPropertyValue: "label1",
OtherPropertyValue: ":::s0:c1,c2",
},
},
expectedAddedVolumes: []addedVolume{
{
volumeName: "fake-plugin/pv1",
podKey: cache.ObjectName{Namespace: namespace, Name: "pod1"},
label: "system_u:object_r:container_file_t:label1",
label: ":::s0:c1,c2",
changePolicy: v1.SELinuxChangePolicyMountOption,
csiDriver: "ebs.csi.aws.com", // The PV is a fake EBS volume
},
},
expectedEvents: []string{
`Normal SELinuxLabelConflict SELinuxLabel "label1" conflicts with pod pod2 that uses the same volume as this pod with SELinuxLabel "label2". If both pods land on the same node, only one of them may access the volume.`,
`Normal SELinuxLabelConflict SELinuxLabel "label2" conflicts with pod pod1 that uses the same volume as this pod with SELinuxLabel "label1". If both pods land on the same node, only one of them may access the volume.`,
`Normal SELinuxLabelConflict SELinuxLabel ":::s0:c1,c2" conflicts with pod pod2 that uses the same volume as this pod with SELinuxLabel ":::s0:c98,c99". If both pods land on the same node, only one of them may access the volume.`,
`Normal SELinuxLabelConflict SELinuxLabel ":::s0:c98,c99" conflicts with pod pod1 that uses the same volume as this pod with SELinuxLabel ":::s0:c1,c2". If both pods land on the same node, only one of them may access the volume.`,
},
},
{
@ -231,7 +230,7 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
pvBoundToPVC("pv1", "pvc1"),
},
existingPods: []*v1.Pod{
podWithPVC("pod1", "label1", nil, "pvc1", "vol1"),
podWithPVC("pod1", "s0:c1,c2", nil, "pvc1", "vol1"),
// "pod2" does not exist
},
pod: cache.ObjectName{Namespace: namespace, Name: "pod1"},
@ -240,31 +239,31 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
PropertyName: "SELinuxLabel",
EventReason: "SELinuxLabelConflict",
Pod: cache.ObjectName{Namespace: namespace, Name: "pod1"},
PropertyValue: "label1",
PropertyValue: ":::s0:c1,c2",
OtherPod: cache.ObjectName{Namespace: namespace, Name: "pod2"},
OtherPropertyValue: "label2",
OtherPropertyValue: ":::s0:c98,c99",
},
{
PropertyName: "SELinuxLabel",
EventReason: "SELinuxLabelConflict",
Pod: cache.ObjectName{Namespace: namespace, Name: "pod2"},
PropertyValue: "label2",
PropertyValue: ":::s0:c98,c99",
OtherPod: cache.ObjectName{Namespace: namespace, Name: "pod1"},
OtherPropertyValue: "label1",
OtherPropertyValue: ":::s0:c1,c2",
},
},
expectedAddedVolumes: []addedVolume{
{
volumeName: "fake-plugin/pv1",
podKey: cache.ObjectName{Namespace: namespace, Name: "pod1"},
label: "system_u:object_r:container_file_t:label1",
label: ":::s0:c1,c2",
changePolicy: v1.SELinuxChangePolicyMountOption,
csiDriver: "ebs.csi.aws.com", // The PV is a fake EBS volume
},
},
expectedEvents: []string{
// Event for the missing pod is not sent
`Normal SELinuxLabelConflict SELinuxLabel "label1" conflicts with pod pod2 that uses the same volume as this pod with SELinuxLabel "label2". If both pods land on the same node, only one of them may access the volume.`,
`Normal SELinuxLabelConflict SELinuxLabel ":::s0:c1,c2" conflicts with pod pod2 that uses the same volume as this pod with SELinuxLabel ":::s0:c98,c99". If both pods land on the same node, only one of them may access the volume.`,
},
},
{
@ -283,7 +282,6 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, ctx := ktesting.NewTestContext(t)
seLinuxTranslator := util.NewFakeSELinuxLabelTranslator()
_, plugin := volumetesting.GetTestKubeletVolumePluginMgr(t)
plugin.SupportsSELinux = true
@ -307,8 +305,6 @@ func TestSELinuxWarningController_Sync(t *testing.T) {
if err != nil {
t.Fatalf("failed to create controller: %v", err)
}
// Use the fake translator, it pretends to support SELinux on non-selinux systems
c.seLinuxTranslator = seLinuxTranslator
// Use a fake volume cache
labelCache := &fakeVolumeCache{
conflictsToSend: map[cache.ObjectName][]volumecache.Conflict{
@ -420,11 +416,11 @@ func pvcBoundToPV(pvName, pvcName string) *v1.PersistentVolumeClaim {
return pvc
}
func pod(podName, label string, changePolicy *v1.PodSELinuxChangePolicy) *v1.Pod {
func pod(podName, level string, changePolicy *v1.PodSELinuxChangePolicy) *v1.Pod {
var opts *v1.SELinuxOptions
if label != "" {
if level != "" {
opts = &v1.SELinuxOptions{
Level: label,
Level: level,
}
}
return &v1.Pod{

View File

@ -0,0 +1,97 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package translator
import (
"strings"
v1 "k8s.io/api/core/v1"
"k8s.io/kubernetes/pkg/volume/util"
)
// ControllerSELinuxTranslator is implementation of SELinuxLabelTranslator that can be used in kube-controller-manager (KCM).
// A real SELinuxLabelTranslator would be able to file empty parts of SELinuxOptions from the operating system defaults (/etc/selinux/*).
// KCM often runs as a container and cannot access /etc/selinux on the host. Even if it could, KCM can run on a different distro
// than the actual worker nodes.
// Therefore do not even try to file the defaults, use only fields filed in the provided SELinuxOptions.
type ControllerSELinuxTranslator struct{}
var _ util.SELinuxLabelTranslator = &ControllerSELinuxTranslator{}
func (c *ControllerSELinuxTranslator) SELinuxEnabled() bool {
// The controller must have been explicitly enabled, so expect that all nodes have SELinux enabled.
return true
}
func (c *ControllerSELinuxTranslator) SELinuxOptionsToFileLabel(opts *v1.SELinuxOptions) (string, error) {
if opts == nil {
return "", nil
}
// kube-controller-manager cannot access SELinux defaults in /etc/selinux on nodes.
// Just concatenate the existing fields and do not try to default the missing ones.
parts := []string{
opts.User,
opts.Role,
opts.Type,
opts.Level,
}
label := strings.Join(parts, ":")
if label == ":::" {
// Empty SELinuxOptions should have the same behavior as nil
return "", nil
}
return label, nil
}
// Conflicts returns true if two SELinux labels conflict.
// These labels must be generated by SELinuxOptionsToFileLabel above
// (the function expects strict nr. of elements in the labels).
// Since this translator cannot default missing components,
// the missing components are treated as incomparable and they do not
// conflict with anything.
// Example: "system_u:system_r:container_t:s0:c1,c2" *does not* conflict with ":::s0:c1,c2",
// because the node that will run such a Pod may expand "":::s0:c1,c2" to "system_u:system_r:container_t:s0:c1,c2".
// However, "system_u:system_r:container_t:s0:c1,c2" *does* conflict with ":::s0:c98,c99".
func (c *ControllerSELinuxTranslator) Conflicts(labelA, labelB string) bool {
partsA := strings.SplitN(labelA, ":", 4)
partsB := strings.SplitN(labelB, ":", 4)
// Reorder, so partsA is always longer than partsB
if len(partsA) < len(partsB) {
partsB, partsA = partsA, partsB
}
for len(partsB) < len(partsA) {
partsB = append(partsB, "")
}
for i := range partsA {
if partsA[i] == partsB[i] {
continue
}
if partsA[i] == "" {
// incomparable part, no conflict
continue
}
if partsB[i] == "" {
// incomparable part, no conflict
continue
}
// Parts are not equal and neither of them is "" -> conflict
return true
}
return false
}

View File

@ -0,0 +1,141 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package translator
import (
"testing"
v1 "k8s.io/api/core/v1"
)
func TestSELinuxOptionsToFileLabel(t *testing.T) {
tests := []struct {
name string
opts *v1.SELinuxOptions
expected string
}{
{
name: "nil options",
opts: nil,
expected: "",
},
{
name: "all fields set",
opts: &v1.SELinuxOptions{
User: "system_u",
Role: "system_r",
Type: "container_t",
Level: "s0:c0,c1",
},
expected: "system_u:system_r:container_t:s0:c0,c1",
},
{
name: "some fields set",
opts: &v1.SELinuxOptions{
Type: "container_t",
Level: "s0:c0,c1",
},
expected: "::container_t:s0:c0,c1",
},
{
name: "only level set",
opts: &v1.SELinuxOptions{
Level: "s0:c0,c1",
},
expected: ":::s0:c0,c1",
},
{
name: "no fields set",
opts: &v1.SELinuxOptions{},
expected: "",
},
}
translator := &ControllerSELinuxTranslator{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := translator.SELinuxOptionsToFileLabel(tt.opts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result)
}
})
}
}
func TestLabelsConflict(t *testing.T) {
tests := []struct {
name string
a, b string
conflict bool
}{
{
name: "empty strings don't conflict",
a: "",
b: "",
conflict: false,
},
{
name: "empty string don't conflict with anything",
a: "",
b: "system_u:system_r:container_t",
conflict: false,
},
{
name: "empty parts don't conflict with anything",
a: ":::::::::::::",
b: "system_u:system_r:container_t",
conflict: false,
},
{
name: "different lengths don't conflict if the common parts are the same",
a: "system_u:system_r:container_t:c0,c2",
b: "system_u:system_r:container_t",
conflict: false,
},
{
name: "different lengths conflict if the common parts differ",
a: "system_u:system_r:conflict_t:c0,c2",
b: "system_u:system_r:container_t",
conflict: true,
},
{
name: "empty parts with conflict",
a: "::conflict_t",
b: "::container_t",
conflict: true,
},
{
name: "non-conflicting empty parts",
a: "system_u::container_t",
b: ":system_r::c0,c2",
conflict: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := ControllerSELinuxTranslator{}
ret := c.Conflicts(test.a, test.b)
if ret != test.conflict {
t.Errorf("expected Conflicts(%q, %q) to be %t, got %t", test.a, test.b, test.conflict, ret)
}
})
}
}