Alle Rezepte
Kubernetes · Workload-Security

securityContext:
Pod-Ebene vs. Container-Ebene

Zwei API-Typen, teilweise gleiche Feldnamen, unterschiedliche Wirkung – das ist die Hauptquelle für Verwirrung. Sechs Abschnitte von der Faustregel über die Feldübersicht bis zum Standard-Rezept und den typischen Fehlern.

0GrundideeZwei Orte, eine Faustregel 1Was gehört wohinPod, Container, beide 2Felder im DetailWirkung und Stolpersteine 3Standard-RezeptRestricted, A-C-R-S 4Häufige FehlerFünf Klassiker 5Nachschlagenexplain und Realitätscheck
0

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.

Regel: Container schlägt Pod. Was am Container steht, überschreibt die Pod-Vorgabe – aber nur für diesen einen Container.
Der Pod ist die Hausordnung im Treppenhaus, der Container der Aushang an der Wohnungstür. Wo beide etwas sagen, gilt der Aushang. Manche Dinge – Waschküche, Hausanschluss – kann nur die Hausordnung regeln, weil sie allen gemeinsam gehören.
1

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

FeldWas es macht
fsGroupSetzt eine Gruppen-ID auf gemountete Volumes und die darin erzeugten Dateien
fsGroupChangePolicyOnRootMismatch oder Always – ob bei jedem Start alle Dateirechte neu gesetzt werden
supplementalGroupsZusätzliche Gruppen für alle Container-Prozesse
supplementalGroupsPolicyMerge (Default) oder Strict – ob Gruppen aus /etc/group des Images dazukommen
sysctlsKernel-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

FeldWas es macht
allowPrivilegeEscalationSetzt das Kernel-Flag no_new_privs. Verhindert, dass ein Prozess über setuid-Binaries – klassisch sudo – mehr Rechte bekommt als sein Elternprozess
readOnlyRootFilesystemMountet das Wurzel-Dateisystem des Containers schreibgeschützt
privilegedDer Generalschlüssel: praktisch alle Capabilities plus Zugriff auf die Geräte der Node
capabilitiesEinzelne Kernel-Rechte gezielt geben (add) oder wegnehmen (drop)
procMountWie /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.

Der Klassiker: 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

FeldVerhalten
runAsUserPod = Vorgabe, Container überschreibt
runAsGroupdito. Ohne Angabe ist die primäre Gruppe root (0)
runAsNonRootNur eine Prüfung: startet der Container als UID 0, wird er abgelehnt
seccompProfileSyscall-Filter, RuntimeDefault oder Localhost
seLinuxOptionsSELinux-Labels
appArmorProfileAppArmor-Profil – seit 1.30 eigenes Feld, davor Annotation

Kurz-Referenz zum Merken

FrageEbene
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-Parameter
sysctls
nur Pod
Syscall- und MAC-Filter
seccompProfile, seLinuxOptions, appArmorProfile
beide, Container gewinnt

Der rote Faden: Prozess-Eigenschaften an den Container, gemeinsame Ressourcen an den Pod.

2

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.

Achtung: Wenn das Image keinen non-root User definiert, startet der Pod gar nicht. Dann braucht es zusätzlich ein 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: {}
Merksatz: read-only Root plus 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.

Wichtig: 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.

Nebenwirkung bei großen Volumes: Kubernetes läuft beim Start rekursiv durch alle Dateien. Das kann Minuten dauern. Dagegen hilft 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
3

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: false
  • capabilities.drop: ["ALL"]
  • runAsNonRoot: true
  • seccompProfile.type: RuntimeDefault
Zur Platzierung: runAsNonRoot und seccompProfile dürfen laut Standard auch auf Pod-Ebene stehen. Die anderen beiden müssen an jeden Container.
A-C-R-S – Aufstieg verboten, Capabilities weg, Root verboten, Seccomp an.
4

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.

  1. Alles auf Pod-Ebene schreibenreadOnlyRootFilesystem und allowPrivilegeEscalation werden dort stillschweigend als unbekanntes Feld abgelehnt oder – je nach Client – als Fehler gemeldet. Wirkung: keine.
  2. Nur einen von mehreren Containern anpassenBei zwei Containern brauchen beide ihren eigenen Block. Sidecars und Init-Container zählen mit.
  3. runAsNonRoot: true ohne passendes ImageDer Pod bleibt in CreateContainerConfigError hängen, mit der Meldung, dass der Container als root laufen würde.
  4. readOnlyRootFilesystem ohne emptyDirCrashLoop, oft mit einer wenig aussagekräftigen Fehlermeldung der Anwendung.
  5. privileged: false setzen und sich sicher fühlenOhne capabilities.drop und allowPrivilegeEscalation: false bleiben genug Wege offen.
5

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.