Kubernetes 1.37: noexec, nosuid und emptyDir.mode für beschreibbare Volumes
readOnlyRootFilesystem reicht nicht, wenn beschreibbare Volumes Binaries erlauben. Was Kubernetes 1.37 als Alpha mitbringt und wie Sie Ihre Workloads jetzt schon prüfen.
readOnlyRootFilesystem: true steht in vielen Security-Baselines ganz oben. In Reviews sehe ich das Feld regelmäßig gesetzt, und direkt darunter ein emptyDir nach /tmp, ein Upload-Verzeichnis auf einem PersistentVolume oder ein Cache-Pfad, in den die Anwendung schreiben muss.
Das Root-Dateisystem ist dann geschützt. Die beschreibbaren Volumes sind es nicht. Kubernetes 1.37 bringt zwei Alpha-Features, mit denen sich genau diese Fläche einschränken lässt: bindMountOptions an VolumeMounts und ein mode-Feld für emptyDir. Beide sind standardmäßig abgeschaltet. Für die Produktion ist das also noch nichts, für die Bestandsaufnahme in Ihren Clustern aber ein guter Anlass.
Was readOnlyRootFilesystem abdeckt und was nicht
readOnlyRootFilesystem betrifft das Dateisystem aus dem Container-Image. Jedes Volume, das in den Container gemountet wird, hat seine eigenen Mount-Flags. Und die sind bisher ziemlich offen: Container Runtime und kubelet binden Volumes ohne noexec, nosuid und nodev ein.
Für einen kompromittierten Prozess heißt das: Er lädt ein Binary in ein beschreibbares Volume, setzt chmod +x und startet es. Der schreibgeschützte Root-Pfad hält ihn dabei nicht auf.
Neu ist die Lücke nicht. Schon 2017 wurde in Issue #48912 bemängelt, dass emptyDir mit medium: Memory ein tmpfs ohne nosuid, nodev und noexec einhängt. Im Security Audit zu Kubernetes 1.24 tauchte das Thema als Finding NCC-E003660-7HM wieder auf (Issue #119627): emptyDir-Volumes unterstützen keine Mount-Optionen. Wer in regulierten Umgebungen arbeitet, kennt diese Art Prüffrage. Bis 1.37 gab es darauf keine native Antwort.
Dazu kommt ein zweites Detail: Ein emptyDir entsteht immer mit den Rechten 0777. Jeder Prozess im Pod kann darin lesen, schreiben und auch fremde Dateien löschen.
Die zwei neuen Felder
bindMountOptions (Feature Gate VolumeBindMountOptions, KEP-5855) hängt am einzelnen VolumeMount. Erlaubt sind genau drei Werte: noexec, nosuid und nodev.
containers:
- name: app
image: registry.k8s.io/busybox
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
bindMountOptions:
- noexec
- nosuid
volumes:
- name: tmp
emptyDir: {}
Das Feld funktioniert mit emptyDir, PersistentVolumes, CSI-Volumes, projected Volumes, ConfigMaps und Secrets. Einzige Ausnahme laut Upstream sind Image Volumes.
Wichtig ist die Abgrenzung zu mountOptions am PersistentVolume. Die gelten auf Storage-Ebene und werden vom CSI-Treiber am Node gesetzt. Beim Bind Mount in den Container kommen sie nicht zuverlässig an. bindMountOptions setzt dagegen die Container Runtime direkt am Bind Mount. Die beiden Felder arbeiten auf verschiedenen Ebenen und kommen sich nicht in die Quere.
emptyDir.mode (Feature Gate EmptyDirVolumeMode, KEP-5502) legt die Unix-Rechte des Verzeichnisses fest, erlaubt sind Werte von 0000 bis 01777. Ohne Angabe bleibt es bei 0777.
volumes:
- name: shared-tmp
emptyDir:
mode: 01777
Mit 01777 bekommt das Volume das Sticky Bit wie ein klassisches /tmp: Eine Datei darf dann nur ihr Eigentümer löschen oder umbenennen, außerdem der Eigentümer des Verzeichnisses und root. Mit 0750 lässt sich der Zugriff auf Eigentümer und Gruppe begrenzen. Das Feld gilt für alle Medium-Typen, also Disk, Memory (tmpfs) und HugePages. Bisher ging das nur mit einem Init-Container, der chmod ausführt. Das funktioniert, ist aber umständlich und in einem Audit schwer nachzuweisen.
Was die Features nicht leisten
Der Kubernetes-Blog formuliert an einer Stelle, ein kompromittierter Workload könne mit noexec die geladene Nutzlast nicht ausführen. Ganz so einfach ist es nicht.
noexec verhindert, dass der Kernel eine Datei auf diesem Mount direkt als Programm startet. Ein Skript, das über einen Interpreter aufgerufen wird, also sh /tmp/payload.sh oder python3 /tmp/payload.py, läuft weiter, denn ausgeführt wird dabei der Interpreter aus dem Image. Je weniger Interpreter und Werkzeuge im Image stecken, desto mehr bringt noexec. Bei einem Distroless-Image hilft es viel, bei einem vollen Basis-Image mit Shell, Python und Paketmanager deutlich weniger.
nosuid ist wertvoll, überschneidet sich aber mit einer Einstellung, die Sie vermutlich schon haben. Mit allowPrivilegeEscalation: false setzt die Runtime no_new_privs für den Prozess, und setuid-Bits greifen dann ohnehin nicht mehr. Als zweite Schicht hat nosuid trotzdem seinen Platz, vor allem bei Workloads, die aus gutem Grund ohne diese Einstellung laufen.
Das Sticky Bit schützt nur zwischen verschiedenen Benutzern. Laufen alle Container im Pod mit derselben UID, und das ist eher die Regel als die Ausnahme, kann jeder Container weiterhin die Dateien der anderen löschen. Das Szenario “Sidecar darf die Artefakte des Builders nicht anfassen” funktioniert also nur, wenn Sie die UIDs der Container bewusst trennen.
Und wenn fsGroup im Security Context des Pods gesetzt ist, überschreiben die Gruppenrechte aus fsGroup den mode des Volumes. Das ist dasselbe Verhalten wie bei defaultMode an Secret- und ConfigMap-Volumes. Ein 0750 im Manifest heißt dann nicht automatisch 0750 im Container.
Voraussetzungen und Version Skew
Beide Gates müssen auf dem API-Server und auf dem kubelet aktiv sein. Beim Version Skew verhalten sich die Features unterschiedlich, und das ist für Nachweise relevant.
Für bindMountOptions muss die Container Runtime das Feld mount_options in der CRI-Nachricht Mount unterstützen und über runtimeFeatures melden. Der Scheduler nutzt Node Declared Features, um Pods nicht auf Nodes ohne diese Unterstützung zu setzen. Kommt ein Pod trotzdem dort an, lehnt der kubelet ihn ab. Die Optionen werden also nicht stillschweigend ignoriert.
Bei emptyDir.mode ist das anders. Kennt der API-Server das Feld, der kubelet aber nicht, wird das Feld angenommen und ignoriert. Das Verzeichnis entsteht dann mit 0777, ohne Fehlermeldung. Für ein Audit reicht ein Blick ins Manifest deshalb nicht, geprüft wird im laufenden Container.
Beide Features wirken nur auf Linux-Nodes. Auf Windows hat bindMountOptions keine Wirkung, und mode wird übersprungen.
Welche containerd- und CRI-O-Versionen mount_options unterstützen, nennen weder der Blogbeitrag noch die Dokumentation. Das müssen Sie in der Doku Ihrer Runtime nachlesen oder im Testcluster ausprobieren. Offen ist bisher auch das Verhalten bei subPath-Mounts und das Zusammenspiel mit SELinux, das in 1.37 ohnehin neu geregelt ist. Mehr dazu steht im Abschnitt zum SELinux-Volume-Labeling in Kubernetes 1.37 und der 429.
In vielen Managed-Kubernetes-Angeboten lassen sich Alpha-Gates gar nicht oder nur in speziellen Testclustern setzen. Wer eine Distribution wie OpenShift betreibt, wartet auf die Freigabe durch den Hersteller.
So prüfen Sie das im Testcluster
Ein kleiner Test-Pod reicht. Er mountet ein emptyDir nach /tmp, einmal mit bindMountOptions: [noexec, nosuid] und einmal mit mode: 01777.
Zuerst die Mount-Flags im Container:
kubectl exec <pod> -- mount | grep /tmp
In der Ausgabe müssen noexec und nosuid auftauchen. Dieser Schritt ist wichtiger als er aussieht: Er zeigt, ob die Runtime die Option wirklich anwendet und nicht nur annimmt.
Dann der Ausführungsversuch:
kubectl exec <pod> -- sh -c 'cp /bin/ls /tmp/ls && /tmp/ls'
Erwartet ist Permission denied. Zum Vergleich lohnt sich sh -c 'echo "echo läuft" > /tmp/t.sh && sh /tmp/t.sh'. Das läuft durch und zeigt die Grenze aus dem vorigen Abschnitt.
Für den mode:
kubectl exec <pod> -- ls -ld /tmp
Erwartet ist drwxrwxrwt. Das t am Ende ist das Sticky Bit. Setzen Sie anschließend fsGroup im Pod und wiederholen Sie die Prüfung, dann sehen Sie, was davon im Container ankommt.
Was Sie jetzt schon tun können
Auch ohne aktivierte Gates lohnt sich die Inventur, weil sie die Grundlage für die spätere Umstellung ist:
- Welche Workloads mounten beschreibbare Volumes, und an welchen Pfaden?
- Welche davon müssen wirklich Dateien ausführen? Bei
/tmp, Upload-Verzeichnissen und Caches ist die Antwort fast immer nein. - Welche
emptyDir-Volumes teilen sich mehrere Container, und laufen diese mit unterschiedlichen UIDs? - Wo ist
allowPrivilegeEscalation: falsenoch nicht gesetzt? - Wie viele Interpreter und Werkzeuge liegen in den Images, die
noexecspäter aushebeln könnten?
Gerade bei Bestandsanwendungen aus Java-, Tomcat- oder .NET-Umgebungen stecken in solchen Pfaden oft historisch gewachsene Annahmen. Wer sie jetzt dokumentiert, kann die Felder später gezielt setzen, sobald sie Beta erreichen und die eigene Plattform sie freigibt.
Wollen Sie wissen, wie es in Ihrem Cluster aussieht?
Welche beschreibbaren Volumes in Ihrem Cluster Ausführung erlauben und welche Rechte Ihre emptyDir-Volumes tatsächlich haben, lässt sich nicht aus der Dokumentation ablesen. Das braucht einen Blick in die laufenden Workloads.
Ich bin Björn Ohlrich und begleite seit über 20 Jahren Infrastruktur-Projekte, seit einigen Jahren mit Schwerpunkt Kubernetes On-Premise in mittelständischen und regulierten Umgebungen.
Genau diese Durchsicht ist Teil des Kubernetes Review: eine strukturierte Bewertung Ihres Setups nach Sicherheit, Stabilität, Performance und Architektur, mit konkreten Handlungsempfehlungen.
Lassen Sie uns unverbindlich sprechen.
Quellen
- Kubernetes Blog, 16.09.2026: Kubernetes v1.37: Hardening Container Storage with Bind Mount Options and EmptyDir Permissions
- Kubernetes-Dokumentation: Set Bind Mount Options on Volume Mounts
- Kubernetes-Dokumentation: Set Permissions on an EmptyDir Volume
- KEP-5855: Bind Mount Options
- KEP-5502: emptyDir Permission Mode
- GitHub: Issue #48912 und Issue #119627
Björn Ohlrich
Kubernetes & AI Ops Consultant