Skip to main content

Kubernetes v1.37 Finally Lets You noexec a Volume Mount: Closing the 9-Year-Old Hole in readOnlyRootFilesystem

8 min readDora NodaDora Noda
Share
On this page

Your read-only root filesystem had a hole in it. For nine years, a compromised process in a locked-down containerreadOnlyRootFilesystem: true, non-root user, dropped capabilities, the works — could still download a binary onto any writable volume, chmod +x it, and execute it. The root filesystem was read-only; the scratch space was a free-fire zone, and Kubernetes gave you no native way to change that.

On September 16, 2026, the Kubernetes project announced the fix: v1.37 ships two alpha features — bindMountOptions on volume mounts and a mode field on emptyDir — behind the VolumeBindMountOptions and EmptyDirVolumeMode feature gates. Together they let you mount a volume noexec, nosuid, nodev, and stop emptyDir from being born world-writable. If you run untrusted tenant workloads on shared nodes, this is the most consequential storage-security change since read-only root filesystems themselves. Here is what changed, what it blocks, and the rollout checklist to work through before you promise any of it to tenants.

Nine years from issue to fix

The paper trail on this gap is unusually long, which is worth appreciating before touching the YAML.

In July 2017, against Kubernetes v1.7, issue #48912 reported that emptyDir with medium: Memory mounts its tmpfs without nosuid, nodev, or noexec — flags the reporter noted every tmpfs should carry. The issue stayed open. Years later, the Kubernetes 1.24 security audit produced finding NCC-E003660-7HM: external auditors called the inability to mount emptyDir with noexec a security failure outright. Google's own container best-practices documentation told users the quiet part out loud: Kubernetes doesn't support mount options on emptyDir, so you cannot mount the volume noexec, meaning an attacker could drop a binary there and run it.

Why did it take so long? The gap wasn't one missing flag — it was a missing layer. PersistentVolumes have long had a mountOptions field, but those options are filesystem-level flags applied by the CSI driver at the node; they do not reliably translate into bind-mount flags inside the container. And the bind mount that actually places a volume into a container is created by the container runtime over CRI, whose mount API had no options field at all. Fixing this properly required a new API field, a CRI addition, runtime support, and scheduler awareness — four layers moving together, owned by SIG Node and SIG Storage jointly (KEP-5855 and KEP-5502). That coordination cost is the real explanation for the nine-year wait, and it's why the fix arrives as two separate alpha gates rather than one switch.

Knob 1: bindMountOptions on the volume mount

The first knob is the headline act. bindMountOptions lives on the volumeMount, next to mountPath, and takes the three classic VFS flags:

yaml
volumeMounts:
  - name: temp-storage
    mountPath: /tmp
    bindMountOptions:
      - noexec
      - nosuid

At the kernel level, each flag does exactly what a Linux operator expects: noexec refuses direct execution of any binary on the mount (MS_NOEXEC at the bind-mount level), nosuid makes setuid/setgid bits inert, and nodev stops device nodes from being interpreted. The canonical verification is three lines: write a script to the mount, chmod +x it, try to run it — and get Permission denied back from the kernel even though the file itself is executable.

Two scope facts matter for fleet planning. First, coverage is broad: bindMountOptions works with emptyDir, PersistentVolumes, CSI volumes, projected volumes, ConfigMaps, and Secrets. The single exception is image volumes, which are explicitly unsupported. Second, this is deliberately not PV mountOptions renamed. The storage-layer options and the new bind-mount flags operate at different layers and do not conflict — you can (and for defense in depth, should) set both where both apply.

Note what this still doesn't do: noexec blocks direct execution from the mount. It doesn't stop an attacker from piping a script into an already-running interpreter (sh /tmp/evil.sh still works if sh is available) — that's what seccomp, AppArmor, and not shipping a shell are for. Treat noexec mounts as one layer that finally closes the drop-and-run path, not as execution prevention by itself.

Knob 2: mode on emptyDir

The second knob fixes the other half of the embarrassment: every emptyDir, in every pod, on every cluster, was created with a hardcoded mode of 0777. Any process that could find the volume could read, write, and delete anything in it regardless of who created it. Multi-container pods sharing scratch space had no native way to stop one container from deleting another's files — the sticky bit solves exactly this, but there was no native way to set it. Workarounds meant init containers running chmod, which is complexity you can't easily verify for compliance.

v1.37 adds a mode field to the emptyDir source:

yaml
volumes:
  - name: shared-tmp
    emptyDir:
      mode: 01777

The two modes to memorize are 01777 — classic /tmp semantics, where the sticky bit restricts deletion to each file's owner — and 0750, for least-privilege application data only one user and group may touch. Verification is pure Unix: ls -ld /tmp shows drwxrwxrwt (note the t), and deleting another user's file fails with Operation not permitted. The mode field works across all emptyDir mediums: disk-backed default, Memory (tmpfs), and HugePages.

One gotcha to internalize now: if the pod sets fsGroup in its security context, the group permissions fsGroup applies will override the emptyDir mode — the same precedence that already exists for defaultMode on Secret and ConfigMap volumes. If your platform injects fsGroup automatically (many do, for shared-volume group ownership), audit that interaction before you advertise 0750 semantics to tenants, or the mode you promised won't be the mode that's enforced.

The fleet rollout checklist: nothing here is on by default

Both features are alpha in v1.37, both default off, and omitting them preserves exactly today's behavior — including the 0777 default. Alpha plus unchanged defaults means you adopt incrementally, but it also means every item below is a gate between "the feature exists" and "tenants can rely on it":

#Checklist itemWhy it bites if skipped
1Enable both gates on the API server and every kubeletThe fields are accepted or enforced only where the gate is on; a half-enabled fleet has inconsistent behavior node to node
2Verify your runtime supports CRI mount_options and advertises it via runtimeFeaturesbindMountOptions needs runtime cooperation; without it the scheduler steers pods away via node-declared features, and a pod that lands on an incapable node is rejected, not degraded
3Treat emptyDir mode skew as the silent oneUnlike bind-mount options, mode on a gate-off kubelet is accepted and silently ignored — the volume falls back to 0777. Monitor gate rollout per node; this is the one that fails open
4Check fsGroup interplayfsGroup overrides mode. If your admission defaults inject fsGroup, your 0750 doesn't mean 0750
5Scope to LinuxbindMountOptions has no effect on Windows nodes and mode is skipped there — Windows has no Unix permission model to enforce
6Enforce via admission policy, not documentationUntil a Kyverno/CEL policy requires these fields on tenant pods, they are opt-in hardening for the tenants who already read the changelog — i.e., not the ones you're worried about

Item 3 deserves emphasis because the asymmetry is the easiest thing to get wrong: the two knobs fail in opposite directions. bindMountOptions fails closed (kubelet rejects the pod — loud, safe), while emptyDir mode fails open (accepted, ignored, 0777 — silent, unsafe). A rollout plan that verifies "pods still schedule" and calls it done will miss exactly the failure that matters. Verify enforcement per node — kubectl exec the Permission denied and Operation not permitted checks from the upstream post — rather than assuming a gate flag on the API server propagated everywhere.

What this buys a PaaS packing strangers onto shared nodes

For a single-tenant cluster, this is nice hygiene. For a multi-tenant PaaS bin-packing mutually untrusted workloads onto shared nodes, it's a structural upgrade to the tenant-isolation story: the platform can now offer noexec+nosuid scratch mounts and sticky-bit shared workspaces as defaults, enforced by admission policy, instead of asking every tenant to hand-roll init-container chmod hacks that auditors can't verify.

That framing — platform default, not tenant homework — is the right altitude. Tenants shouldn't need to know KEP numbers to be safe on your platform; the platform sets the floor and proves it. These two knobs finally let the floor include "you cannot execute binaries out of scratch space" and "your neighbor's container cannot delete your files," which are table-stakes claims no Kubernetes-based PaaS could natively make until v1.37.

Expect the usual alpha-to-beta journey from here: runtime support matrices to watch, then default-on discussions, then Pod Security Standards potentially absorbing these settings the way readOnlyRootFilesystem became part of the restricted profile. The operators who start now — gates on in staging, admission policies drafted, the fsGroup audit done — will be the ones ready to flip defaults the day these graduate, instead of discovering the skew behaviors in production.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex