mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 14:37:00 +00:00
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:
parent
20b12ad5c3
commit
2050d6fc69
@ -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",
|
||||
|
@ -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{
|
||||
|
@ -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)
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user