Wer Kubernetes auf kind oder minikube gelernt hat und dieselben Manifeste nach OpenShift bringt, erlebt als Erstes oft nicht Ingress oder Routes. Es ist ein Pod, der nicht startet — mit einer Meldung zu Security Context Constraints. Das Image lief woanders. Das YAML wirkt in Ordnung. OpenShift sagt nein.

Diese Reaktion ist normal. OpenShift ist weiterhin Kubernetes, ergänzt aber eine Admission-Schicht, die vanilla-Cluster anders lösen. Security Context Constraints — SCCs — legen fest, welche Pods als root laufen dürfen, welche Linux-Capabilities bleiben, welche Volume-Typen gemountet werden dürfen und ob privileged oder Host-Pfade erlaubt sind. Scheduler und Kubelet zählen, aber bei vielen OpenShift-Fehlern fällt die SCC-Prüfung früher an — bei der Admission.

Dieser Post ist für den Moment, in dem ein Deployment bei null Ready-Replicas hängt und Events unable to validate against any security context constraint erwähnen. Ziel ist nicht, jedes SCC-Feld auswendig zu lernen. Ziel ist zu verstehen, warum SCCs existieren, typische Fehlermuster zu erkennen und Denials zu debuggen, ohne auf anyuid zurückzugreifen, weil es der schnellste Fix war.

Warum SCCs existieren, obwohl es schon securityContext gibt

Kubernetes erlaubt securityContext auf Pod- oder Container-Ebene: als Non-Root-UID laufen, Capabilities droppen, Privilege Escalation verbieten, seccomp wählen. Das ist nötig, aber auf einer gemeinsamen Plattform nicht immer genug. Ein Manifest kann securityContext ganz weglassen und trotzdem einen Pod anfordern. Jemand muss Defaults und Obergrenzen erzwingen.

Pod Security Standards und Pod Security Admission auf upstream Kubernetes schieben Namespaces über Labels Richtung restricted, baseline oder privileged. OpenShift ist älter als vieles davon und hat ein eigenes Modell. SCCs sind clusterweite Objekte. Die Admission prüft jeden Pod gegen die SCCs, die der ServiceAccount des Pods nutzen darf. Die erste passende SCC gewinnt. Passt keine, wird der Pod abgelehnt.

Das dient Platform-Teams. Ein Developer-Namespace kann standardmäßig restricted nutzen, während Infrastruktur-Namespaces nach Policy enger oder lockerer sind. Die Durchsetzung ist zentral. Man umgeht sie nicht, indem man vergisst, ein Namespace zu labeln — wie bei PSA manchmal möglich, wenn niemand Labels gesetzt hat.

SCCs kodieren auch OpenShift-spezifische Themen: welche UIDs auf der Plattform gültig sind, ob Host-Networking erlaubt ist, ob beliebige FSGroup auf Volumes erlaubt ist und wie Supplemental Groups wirken. Wer nur generische Kubernetes-Security-Docs liest, fixt vielleicht runAsUser im YAML und scheitert trotzdem, weil die SCC den Volume-Typ oder die UID-Range nicht erlaubt.

securityContext ist, was der Workload anfordert. Die SCC ist, was die Plattform für diesen ServiceAccount erlaubt. Beides muss zusammenpassen.

Was eine SCC tatsächlich steuert

Eine SCC ist ein Policy-Dokument. Wichtige Felder in oc describe scc restricted und in der Doku:

  • runAsUser — Strategie MustRunAsRange mit min/max UID, oder RunAsAny, oder MustRunAs für feste UID.
  • fsGroup — dasselbe für Volume-Ownership-Gruppen.
  • supplementalGroups — zusätzliche Gruppen im Container-Prozess.
  • allowPrivilegedContainer — ob privileged: true erlaubt ist.
  • allowPrivilegeEscalation — ob ein Prozess mehr Rechte als der Parent bekommen darf.
  • allowedCapabilities / requiredDropCapabilities — welche Linux-Capabilities erlaubt oder gedroppt werden müssen.
  • readOnlyRootFilesystem — ob das Root-Dateisystem read-only sein muss.
  • volumes — erlaubte Typen wie configMap, secret, persistentVolumeClaim, emptyDir, projected; Host-Pfade sind meist eingeschränkt.
  • allowHostNetwork, allowHostPID, allowHostIPC — Host-Namespace-Sharing.
  • seLinuxContext — SELinux-Labeling auf RHEL-CoreOS-Nodes.

Nicht jedes Feld am ersten Tag. Bei Pod-Fehlern starte ich mit runAsUser, fsGroup, Capabilities und volumes. Diese vier decken die meisten Tickets ab, die ich gesehen habe.

SCCs auflisten und Defaults ansehen:

oc get scc
oc describe scc restricted
oc describe scc anyuid
oc describe scc nonroot

Die Namen sind Hinweise, keine Garantien. Immer das live Objekt im Cluster lesen. Upgrades können Defaults anpassen.

restricted, anyuid und wann Custom-SCCs Sinn ergeben

OpenShift liefert mehrere Built-in-SCCs. Drei Namen tauchen ständig auf.

restricted ist in vielen Namespaces der Default. Pods laufen als beliebige UID aus einer plattformdefinierten Range, alle Capabilities werden gedroppt, privileged ist verboten, Volume-Typen sind begrenzt. Entspricht dem Geist von Kubernetes restricted Pod Security. Images, die UID 0 erwarten, nach / schreiben wollen oder CAP_NET_BIND_SERVICE ohne Deklaration brauchen, scheitern hier. Absichtlich.

anyuid erlaubt jede UID, inklusive 0. Bekannter Notausgang, wenn ein Vendor-Image root hardcodiert und schnell nicht rebuildbar ist. Aber breiter als die meisten Apps brauchen. anyuid für einen ServiceAccount löst viele Startfehler und entfernt viel von dem, was restricted schützen soll. Platform-Teams behandeln das oft als geprüfte Ausnahme, nicht als Default pro Namespace.

nonroot liegt im Geist dazwischen: Container muss als Non-Zero-UID laufen, die exakte UID ist in manchen Konfigurationen flexibler als bei restricted. Manche Cluster nutzen es für Legacy-Apps ohne random UID-Range, aber ohne root.

Darüber hinaus erlauben Custom-SCCs Least Privilege für ein Team oder eine App: nur NET_BIND_SERVICE, eine bestimmte UID-Range, emptyDir und PVC aber kein hostPath. Custom-SCCs sind die langfristige Antwort, wenn restricted fast passt und anyuid viel zu weit geht.

Strategien im Vergleich:

# Fragment — restricted-style (conceptual; real SCCs are cluster objects, not Pod YAML)
runAsUser:
  type: MustRunAsRange
  uidRangeMin: 1000660000
  uidRangeMax: 1000669999
requiredDropCapabilities:
  - KILL
  - MKNOD
  - SETUID
  - SETGID
allowPrivilegedContainer: false
volumes:
  - configMap
  - downwardAPI
  - emptyDir
  - persistentVolumeClaim
  - projected
  - secret
# Fragment — anyuid-style (conceptual)
runAsUser:
  type: RunAsAny
allowPrivilegedContainer: false
# Still not the same as privileged; read the full SCC

Schlägt jemand anyuid vor, frage ich, was konkret unter restricted scheiterte. Antwort „läuft als root“ — nächste Frage, ob das Image mit Non-Root-User rebuildbar ist. Antwort „bindet Port 80“ — Fix kann Custom-SCC mit NET_BIND_SERVICE oder Service targetPort über 1024 sein — nicht volles root.

Wie SCCs an den Pod gebunden werden

Pods referenzieren keine SCC by name im Manifest. Die Admission leitet erlaubte SCCs vom ServiceAccount ab. OpenShift bindet SCCs an User und Groups über RBAC-ähnliche Ressourcen: ClusterRoleBindings wie system:openshift:scc:restricted erlauben die Nutzung einer benannten SCC.

Typischer Ablauf:

  1. Ein Pod (oder Deployment) wird mit serviceAccountName: myapp erstellt.
  2. Admission lädt SCCs, die myapp nutzen darf.
  3. Admission matcht Pod-securityContext und Spec-Felder gegen jede SCC.
  4. Das erste gültige Match landet in der Pod-Annotation openshift.io/scc.

Welche SCC ein laufender Pod bekam:

oc get pod myapp-7d4f8b9c6-xk2lm -o jsonpath='{.metadata.annotations.openshift\.io/scc}'; echo
oc get pod myapp-7d4f8b9c6-xk2lm -o yaml | grep -A2 openshift.io/scc

Welche SCCs ein ServiceAccount nutzen darf:

oc adm policy who-can use scc restricted -n myproject
oc describe sa myapp -n myproject
oc get rolebinding,clusterrolebinding -n myproject | grep myapp

Neue Namespaces bekommen oft restricted für Default-ServiceAccounts über system:openshift:scc:restricted. Nutzt der Pod einen Custom-ServiceAccount ohne SCC-Binding, matcht er vielleicht nichts — typische Story „läuft mit default, scheitert mit eigenem SA“.

Einem ServiceAccount SCC-Zugriff geben (Platform-Admin):

oc adm policy add-scc-to-user restricted -z myapp -n myproject
oc adm policy add-scc-to-user anyuid -z legacy-batch -n myproject

Lieber Custom-SCC an eine Gruppe binden als anyuid auf viele einzelne Accounts streuen, wenn viele Workloads dieselbe Lücke haben.

Typische Pod-Fehler, die wie App-Bugs wirken

SCC-Denials erscheinen oft als CreateContainerConfigError, FailedCreate oder Deployments mit null Ready-Replicas. Der Container startet nie — keine App-Logs, nur Events.

Als root laufen. Dockerfile endet mit USER root oder ohne USER, Entrypoint erwartet UID 0. restricted lehnt ab. Fix: Image Non-Root rebuilden oder securityContext.runAsUser auf erlaubte Range setzen, wenn das Image beliebige UIDs unterstützt.

Feste UID außerhalb der Range. Manche Images erwarten UID 1001, weil der Vendor das sagt. OpenShifts Range enthält 1001 vielleicht nicht. Fix: runAsUser in Range anpassen oder Custom-SCC mit passender Range.

Beschreibbares Root-Dateisystem. readOnlyRootFilesystem: true in SCC oder Pod kollidiert mit Apps, die Logs oder Temp unter / schreiben. Fix: emptyDir für /tmp oder Logs, Pfade im Image anpassen, oder read-only in Custom-SCC begründet lockern.

Capabilities. Bindung an Ports unter 1024 ohne NET_BIND_SERVICE, oder Images mit CHOWN/SETUID. Fix: Capability-Bedarf entfernen (8080 lauschen) oder nur diese Capability in Custom-SCC erlauben.

Verbotene Volume-Typen. hostPath, NFS in manchen Setups, exotische Plugins. Fix: PVC, ConfigMap oder Secret; hostPath auf Worker-Nodes ist für App-Teams selten sinnvoll.

FSGroup und Volume-Rechte. Pod setzt fsGroup, die die SCC verbietet, oder Storage-Rechte passen nicht zur UID. Fix: fsGroup an SCC-Strategie anpassen und PVC-Mount-Rechte prüfen.

Deployment-Beispiel, das unter restricted oft ohne root funktioniert:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: myproject
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      serviceAccountName: web
      containers:
        - name: app
          image: registry.example.org/shop/web:2.4.1
          ports:
            - containerPort: 8080
          securityContext:
            allowPrivilegeEscalation: false
            runAsNonRoot: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}

Scheitert der Pod trotzdem, startet das Image intern vielleicht noch als root. oc describe pod und Image-Metadaten prüfen — nicht nur das eigene YAML.

SCC-Denials Schritt für Schritt debuggen

Erwähnen Events SCC-Validierung, ist es ein Admission-Problem. Debugging ist wiederholbar.

1. Zuerst Events lesen.

oc describe pod failing-pod-name -n myproject
oc get events -n myproject --sort-by='.lastTimestamp' | tail -20

Text wie unable to validate against any security context constraint oder provider restricted: .containers[0].runAsUser: Invalid value. Der Provider-Name zeigt, welche SCC fast passte und welches Feld scheiterte.

2. Pod-Spec mit SCC vergleichen.

oc get pod failing-pod-name -n myproject -o yaml > /tmp/pod.yaml
oc describe scc restricted

Feld für Feld: UID-Strategie, Capabilities, Volumes, privileged, Host-Namespaces.

3. SCC-Rechte des ServiceAccounts prüfen.

oc describe sa web -n myproject
oc adm policy who-can use scc anyuid -n myproject
oc auth can-i use scc/restricted --as=system:serviceaccount:myproject:web -n myproject

Kann der ServiceAccount keine SCC nutzen, die zum Pod passt, scheitert Admission — auch wenn der Pod-Spec sonst vernünftig wirkt.

4. Audit-Logs nutzen, wo Zugriff besteht. OpenShift-Audit kann SCC-Entscheidungen detaillierter protokollieren als Events. Das Platform-Team führt die Query oft aus; der Trail existiert trotzdem.

5. Mit minimalem Pod reproduzieren. Init-Container, Volumes und Sidecars streichen, bis ein nackter Pod scheitert oder läuft. Stück für Stück zurückbauen. SCC-Fehler liest man leichter auf einem Fünf-Zeiler als in einem Helm-Chart mit zwölf Templates.

oc run scc-test --image=registry.example.org/shop/web:2.4.1 \
  --restart=Never -n myproject --overrides='
{
  "spec": {
    "serviceAccountName": "web",
    "containers": [{
      "name": "scc-test",
      "image": "registry.example.org/shop/web:2.4.1"
    }]
  }
}'
oc describe pod scc-test -n myproject
oc delete pod scc-test -n myproject

6. In der richtigen Reihenfolge fixen. Zuerst Pod-Spec und Image. Dann Custom-SCC. Dann mit Review ein breiteres Built-in wie anyuid. Begründung dokumentieren.

Least Privilege, wenn restricted fast reicht

Platform-Security ist nicht nur Debatte zwischen restricted und anyuid. Reife Teams investieren in Custom-SCCs und Image-Hardening.

Zuerst härten: Non-Root USER im Dockerfile, auf unprivilegierten Ports lauschen, Temp-Daten auf gemountete Volumes, benötigte Capabilities dokumentieren. Viele Commercial Images sind runAsUser: auto-freundlich, wenn man die OpenShift-Anleitung des Vendors liest.

Bei Custom-SCC das nächste Built-in kopieren und eine Sache enger oder weiter fassen:

allowHostDirVolumePlugin: false
allowHostIPC: false
allowHostNetwork: false
allowHostPID: false
allowHostPorts: false
allowPrivilegeEscalation: false
allowPrivilegedContainer: false
allowedCapabilities:
  - NET_BIND_SERVICE
defaultAddCapabilities: null
requiredDropCapabilities:
  - KILL
  - MKNOD
  - SETUID
  - SETGID
runAsUser:
  type: MustRunAsRange
  uidRangeMin: 1000660000
  uidRangeMax: 1000669999
users: []
groups: []

Mit Platform-Prozess anwenden: SCC clusterweit anlegen, an Gruppe binden:

oc create -f custom-net-bind-scc.yaml
oc adm policy add-scc-to-group custom-net-bind-scc system:serviceaccounts:myproject

Custom-SCCs periodisch reviewen. Eine SCC für ein Release überlebt oft die Begründung. NET_BIND_SERVICE entfernen, wenn die App auf 8080 wechselt. anyuid entfernen, wenn das Image rebuildbar ist.

Für den Alltag kurze Notiz im Repo: benötigter securityContext, welche SCC der ServiceAccount nutzt, warum. Beim Incident soll man die Batch-Job-Ausnahme nicht neu erfinden.

Praktische Checkliste

Bevor ein Platform-Ticket oder anyuid:

  1. Pod-Events lesen — scheiternde SCC und Feld identifizieren.
  2. Prüfen, ob der ServiceAccount mindestens eine passende SCC nutzen darf.
  3. Image-User, Ports und Volume-Writes prüfen — nicht nur Deployment-YAML.
  4. runAsNonRoot, gedroppte Capabilities und emptyDir für Temp-Pfade versuchen.
  5. Custom-SCC statt anyuid, wenn nur Capability oder UID-Range fehlt.
  6. Ausnahme dokumentieren: wer genehmigt, Review-Datum, Owner-Team.

SCCs sind kein willkürlicher OpenShift-Eigenbau. Sie sind die clusterweite Antwort darauf, welche Laufzeit-Rechte jeder Workload haben darf. Liest man Denials als strukturiertes Feedback statt als Rätsel, wirkt OpenShift-Security weniger wie eine Mauer und mehr wie eine Checkliste — streng, aber lernbar.