Kubernetes v1.37 lands on 26 August 2026, and with it SELinuxMount goes GA and on by default. Concretely: if your nodes run SELinux in enforcing mode and you have two pods with different SELinux levels sharing one PersistentVolumeClaim on one node, one of them stops starting. No manifest changed. No image changed. The kubelet just reports conflicting SELinux labels on the volume and the pod sits in ContainerCreating until its neighbour goes away. My position, before anything else: this is the correct change, and I would rather you take the break on a staging cluster this week than switch the affected workloads to seLinuxChangePolicy: Recursive and call it handled.

terminal
kubectl get csidrivers -o custom-columns='NAME:.metadata.name,SELINUX_MOUNT:.spec.seLinuxMount'

The mechanics are worth ten seconds because they explain why there is no middle ground. The old path had the container runtime walk the volume and relabel every inode at pod start, cost proportional to how many files you have, paid again on every restart. Anyone who has watched a pod with a fat PVC sit in ContainerCreating on a remote filesystem has probably paid that bill without knowing its name. The new path mounts the volume with a context option and lets the kernel apply one label to the whole superblock. Constant time. But a superblock carries exactly one SELinux context, so mounting the same filesystem twice with two different contexts gets refused at the kernel level. Red Hat has documented that behavior for well over a decade. Kubernetes is not deciding to break you here, it is stopping asking the kernel for something the kernel was never going to give.

Here is what I find interesting, and it is not the storage plumbing. The pattern that breaks worked only because relabeling was per file. Pod A relabeled the volume to its level, pod B came along and overwrote the labels with its own, and both kept reading. Nobody designed that. It is not in a KEP, it is not in anyone's architecture decision record, it just never fell over. If you run PHP on Kubernetes with a php-fpm deployment and a backup or log-shipping sidecar mounting the same claim under a different security context, you are relying on a side effect that outlived the implementation that produced it. We know this shape from our own ecosystem: every codebase has a spot where something works and the honest answer to why is that it always has.

The strongest argument against my position is not nostalgia, it is two concrete things. First, context mounts refuse per-file label changes. Run chcon inside such a volume and you get Operation not supported, because the label was never written to disk in the first place. If some part of your workload manages its own file labels, Recursive is not a delay tactic for you, it is the correct configuration, and you should set it and stop worrying. Second, and this is the one that bothers me: detection is blind in a way nobody has solved. Kubernetes v1.36 ships a selinux-warning-controller you can switch on with --controllers=*,selinux-warning-controller, and it emits selinux_warning_controller_selinux_volume_conflict with both offending pods as labels. Pair it with the kubelet counter volume_manager_selinux_volume_context_mismatch_warnings_total and you learn which pods conflict right now. Right now is the problem. Neither the controller nor any ad-hoc API scrape sees the pair that only collides after the scheduler co-locates them next Tuesday.

I still land on upgrading, for a reason that has nothing to do with startup latency. Recursive relabeling is what made CVE-2021-25741 exploitable in the shape it had: trick the kubelet into exposing part of the host filesystem, and the runtime helpfully relabels those host files so your pod can read them. Under a context mount the pod is simply denied by policy. That is a security property you hand back every time you sprinkle the opt-out on a workload that did not need it. And there is a nasty follow-on: setting seLinuxChangePolicy: Recursive also stops the warning controller from reporting that pod, by design. So the metric you built the rollout around goes quiet while the deferred work stays exactly where it was. If you do opt something out, put an annotation and a ticket number on it, because the observability will not remember for you.

Most of you will read all this and correctly do nothing, which is fine and worth saying out loud. If /sys/fs/selinux/enforce does not exist on your nodes, the kubelet skips the whole code path. Even with SELinux enforcing, only volumes whose CSIDriver object sets .spec.seLinuxMount to true are in scope, and in-tree that list is just fc and iscsi. emptyDir, configMap, secret and projected volumes keep relabeling recursively, so your config and your credentials are untouched. The AWS EBS CSI driver is a good example of why guessing is bad in both directions: its Helm chart only renders seLinuxMount: true when node.selinux is set, so a default install changes nothing for EBS volumes. Go look at your own values file rather than trusting either half of that sentence.

The bit I keep chewing on is that a point-in-time audit cannot prove a cluster safe, because the failure is a function of scheduling, not of your YAML. You can enumerate today's conflicts in one kubectl call and still get paged in November when a node drain puts two pods together for the first time. Admission policy is the honest answer, whether that is MutatingAdmissionPolicy, a webhook, or Kyverno scoped to the namespaces that genuinely share claims, but that is a policy you have to write before you know which workloads need it. So, colleagues: how would you actually prove that a scheduler-dependent conflict cannot happen in your cluster, short of running the feature gate on staging and waiting? I have not found an answer I trust, and I would like to steal yours.

Practical starting point if you want one command: list your drivers and see which ones opted in. Everything else follows from that answer.