Grundidee: zwei Orte, eine Faustregel
Der securityContext ist kein Kubernetes-Feature, sondern ein Formular, das Kubernetes an die Container-Runtime weiterreicht. Alles darin sind Linux-Grundlagen: Benutzer-ID, Gruppen, Capabilities, Mount-Optionen, Kernel-Flags.
Die Faustregel
Ist die Einstellung etwas, das der Kernel pro Prozess setzt, gehört sie an den Container. Ist es etwas, das für den ganzen Pod gemeinsam gilt – Volumes, Netzwerk, Kernel-Parameter –, gehört sie an den Pod.
Damit beantwortet sich fast jede Zuordnungsfrage von selbst.
Die zwei Orte im YAML
apiVersion: v1
kind: Pod
spec:
securityContext: # ← PodSecurityContext
runAsUser: 1000 # gilt als Vorgabe für ALLE Container
fsGroup: 2000
containers:
- name: app
image: nginx
securityContext: # ← SecurityContext (Container)
runAsUser: 30000 # gewinnt bei Konflikt → 30000, nicht 1000
readOnlyRootFilesystem: true
Zwei verschiedene API-Typen mit teilweise gleichen Feldnamen.
Was gehört wohin
Drei Gruppen: Felder, die es nur am Pod gibt, Felder nur am Container, und solche, die an beiden Stellen erlaubt sind.
Nur Pod-Ebene
| Feld | Was es macht |
|---|---|
fsGroup | Setzt eine Gruppen-ID auf gemountete Volumes und die darin erzeugten Dateien |
fsGroupChangePolicy | OnRootMismatch oder Always – ob bei jedem Start alle Dateirechte neu gesetzt werden |
supplementalGroups | Zusätzliche Gruppen für alle Container-Prozesse |
supplementalGroupsPolicy | Merge (Default) oder Strict – ob Gruppen aus /etc/group des Images dazukommen |
sysctls | Kernel-Parameter für den Pod, z. B. net.core.somaxconn |
Warum nur hier: Volumes und der Netzwerk-Namespace gehören dem Pod, nicht einem einzelnen Container. Es gäbe keine sinnvolle Antwort darauf, welche fsGroup gelten soll, wenn zwei Container im selben Pod verschiedene angeben.
Nur Container-Ebene
| Feld | Was es macht |
|---|---|
allowPrivilegeEscalation | Setzt das Kernel-Flag no_new_privs. Verhindert, dass ein Prozess über setuid-Binaries – klassisch sudo – mehr Rechte bekommt als sein Elternprozess |
readOnlyRootFilesystem | Mountet das Wurzel-Dateisystem des Containers schreibgeschützt |
privileged | Der Generalschlüssel: praktisch alle Capabilities plus Zugriff auf die Geräte der Node |
capabilities | Einzelne Kernel-Rechte gezielt geben (add) oder wegnehmen (drop) |
procMount | Wie /proc gemountet wird – Sonderfall, praktisch nur für verschachtelte Container |
Warum nur hier: Das sind Eigenschaften, die der Kernel beim Start eines Prozesses setzt oder die am Dateisystem eines Containers hängen. Jeder Container startet einzeln, jeder hat sein eigenes Root-Dateisystem.
allowPrivilegeEscalation sucht fast jeder zuerst auf Pod-Ebene. Dort gibt es das Feld nicht – es ist ein reines Prozess-Flag ohne gemeinsamen Elternprozess, auf den man es einmal anwenden könnte.Auf beiden Ebenen
| Feld | Verhalten |
|---|---|
runAsUser | Pod = Vorgabe, Container überschreibt |
runAsGroup | dito. Ohne Angabe ist die primäre Gruppe root (0) |
runAsNonRoot | Nur eine Prüfung: startet der Container als UID 0, wird er abgelehnt |
seccompProfile | Syscall-Filter, RuntimeDefault oder Localhost |
seLinuxOptions | SELinux-Labels |
appArmorProfile | AppArmor-Profil – seit 1.30 eigenes Feld, davor Annotation |
Kurz-Referenz zum Merken
| Frage | Ebene |
|---|---|
Wer bin ich?runAsUser, runAsGroup, runAsNonRoot | beide, Container gewinnt |
Was darf mein Prozess?capabilities, privileged, allowPrivilegeEscalation | nur Container |
Darf ich schreiben?readOnlyRootFilesystem | nur Container |
Wem gehören die Volumes?fsGroup, supplementalGroups | nur Pod |
Kernel-Parametersysctls | nur Pod |
Syscall- und MAC-FilterseccompProfile, seLinuxOptions, appArmorProfile | beide, Container gewinnt |
Der rote Faden: Prozess-Eigenschaften an den Container, gemeinsame Ressourcen an den Pod.
Die wichtigsten Felder im Detail
Was die Felder tatsächlich bewirken – und woran man bei jedem einzelnen typischerweise hängenbleibt.
runAsUser / runAsNonRoot · Wer bin ich?
Ohne Angabe läuft der Container mit dem User aus dem Image – meistens root (UID 0). Container-root ist derselbe Kernel-root wie auf der Node, nur eingeschränkt. Bricht jemand aus, ist er oben angekommen.
runAsNonRoot: true ist die robustere Variante: man muss keine konkrete UID kennen, Kubernetes lehnt einfach jeden Container ab, der als 0 startet.
runAsUser: <irgendwas>.readOnlyRootFilesystem · Darf ich schreiben?
Ein Angreifer kann keine Tools nachinstallieren, keine Binaries austauschen, kein Skript ablegen. Sehr wirksam, sehr billig.
Der übliche Stolperstein: Anwendungen wollen irgendwo schreiben. Lösung sind emptyDir-Volumes an genau diesen Pfaden:
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/nginx
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
emptyDir für die Schreibpfade. Wenn ein Pod nach readOnlyRootFilesystem: true in CrashLoop geht, fehlt fast immer genau so ein Volume.capabilities · Root in Einzelteilen
Linux hat root in etwa 40 Einzelrechte zerlegt. CAP_NET_BIND_SERVICE = darf auf Ports unter 1024 lauschen. CAP_SYS_ADMIN = praktisch root.
Best Practice ist immer dasselbe Muster: alles wegnehmen, dann gezielt zurückgeben.
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
Die Namen werden ohne CAP_-Präfix geschrieben.
privileged · Der Aus-Schalter für alles andere
Ein privilegierter Container ist keine Sicherheitsgrenze mehr, sondern ein Prozess auf der Node mit einem anderen Dateisystem. Damit lässt sich /dev/mem lesen, der Host mounten, Kernel-Module laden.
privileged: true macht allowPrivilegeEscalation automatisch wirkungslos – das Flag ist dann immer true. Dasselbe gilt bei CAP_SYS_ADMIN.allowPrivilegeEscalation · Der unterschätzte Riegel
Ohne dieses Flag nützt eine niedrige UID wenig: Gibt es im Image ein setuid-root-Binary, kann sich ein Prozess damit hocharbeiten. false setzt no_new_privs im Kernel und schließt diesen Weg.
fsGroup · Wem gehören die Volumes?
Klassisches Problem: Der Container läuft als UID 1000, das gemountete Volume gehört root. Der Prozess kann nichts schreiben.
fsGroup: 2000 setzt die Gruppe des Volumes auf 2000 und hängt den Container-Prozess in diese Gruppe. Zusätzlich wird das setgid-Bit gesetzt, damit auch neu erzeugte Dateien der Gruppe gehören.
fsGroupChangePolicy: OnRootMismatch – dann wird nur geprüft, ob das Wurzelverzeichnis schon passt.seccompProfile · Syscall-Filter
Seccomp filtert Systemaufrufe. RuntimeDefault nimmt das Profil der Container-Runtime, das rund 60 gefährliche Syscalls sperrt. Kostet nichts, bricht fast nie etwas, ist Pflicht im Restricted-Standard.
seccompProfile:
type: RuntimeDefault
Das Standard-Rezept
Für praktisch jeden normalen Workload – und gleichzeitig die Umsetzung des Restricted Pod Security Standard.
Der vollständige Block
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10000
fsGroup: 10000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
privileged: false
capabilities:
drop: ["ALL"]
Die vier Pflichtfelder des Restricted-Standards
allowPrivilegeEscalation: falsecapabilities.drop: ["ALL"]runAsNonRoot: trueseccompProfile.type: RuntimeDefault
runAsNonRoot und seccompProfile dürfen laut Standard auch auf Pod-Ebene stehen. Die anderen beiden müssen an jeden Container.Häufige Fehler
Fünf Muster, die immer wieder auftreten – die ersten beiden erzeugen still gar keine Wirkung, die anderen einen Pod, der nicht startet.
- Alles auf Pod-Ebene schreiben
readOnlyRootFilesystemundallowPrivilegeEscalationwerden dort stillschweigend als unbekanntes Feld abgelehnt oder – je nach Client – als Fehler gemeldet. Wirkung: keine. - Nur einen von mehreren Containern anpassenBei zwei Containern brauchen beide ihren eigenen Block. Sidecars und Init-Container zählen mit.
runAsNonRoot: trueohne passendes ImageDer Pod bleibt inCreateContainerConfigErrorhängen, mit der Meldung, dass der Container als root laufen würde.readOnlyRootFilesystemohne emptyDirCrashLoop, oft mit einer wenig aussagekräftigen Fehlermeldung der Anwendung.privileged: falsesetzen und sich sicher fühlenOhnecapabilities.dropundallowPrivilegeEscalation: falsebleiben genug Wege offen.
Nachschlagen und prüfen, was tatsächlich läuft
Welches Feld auf welcher Ebene existiert, lässt sich direkt im Cluster nachschlagen – schneller als jede Website und auch in der Prüfung verfügbar.
Felder nachschlagen mit kubectl explain
kubectl explain pod.spec.securityContext
kubectl explain pod.spec.containers.securityContext
# Ein einzelnes Feld:
kubectl explain pod.spec.containers.securityContext.allowPrivilegeEscalation
# Alles auf einmal, gut zum Überfliegen:
kubectl explain pod.spec.containers.securityContext --recursive
Fehlt ein Feld in der einen Ausgabe und steht in der anderen, ist die Zuordnungsfrage beantwortet.
Realitätscheck am laufenden Pod
Was im Objekt steht:
kubectl get pod <pod> -o jsonpath='{.spec.securityContext}{"\n"}'
kubectl get pod <pod> -o jsonpath='{.spec.containers[*].securityContext}{"\n"}'
Was der Prozess wirklich macht:
kubectl exec <pod> -- id
kubectl exec <pod> -- touch /test # scheitert bei read-only Root
kubectl exec <pod> -- cat /proc/1/status | grep NoNewPrivs # 1 = gesetzt
id ist der ehrlichste Test: Was der Kernel sagt, gilt – nicht was im YAML steht.