Cloudogu Logo

Hallo, wir sind Cloudogu!

Experten für Software-Lifecycle-Management und Prozess­auto­mati­sierung, Förderer von Open-Source- Soft­ware und Entwickler des Cloudogu EcoSystem.

featured image Kubernetes AppOps Security Teil 6: Pod Security Policies (2/2) - Ausnahmen und Fehlersuche

28.10.2020 von Johannes Schnatterer in Software Craftsmanship

Kubernetes AppOps Security Teil 6: Pod Security Policies (2/2) - Ausnahmen und Fehlersuche

+++Der Originalartikel kann hier heruntergeladen werden: Zeitschriftenartikel, veröffentlicht in JavaSPEKTRUM 04/2020.+++

Der vorherige Artikel dieser Serie zeigt die Cluster-weite Umsetzung einer good Practice für Security-Einstellungen in Kubernetes mittels Pod Security Policies (PSP). Darauf aufbauend zeigt dieser Artikel, wann mehrere PSPs Sinn machen können und wie ein Umgang mit diesen aussehen kann. Darüber hinaus werden Tipps zur Fehlersuche gegeben. Auch die in diesem Artikel gezeigten Beispiele können in einer definierten Umgebung ausprobiert werden. Dazu stehen vollständige Beispiele mit Anleitung im Repository „cloudogu/k8s-security-demos“ bei GitHub bereit.

Eine oder mehrere PSPs?

Generell ist der Ansatz nur eine Cluster-weite, restriktive PSP zu definieren, der alle Pods entsprechen, empfehlenswert. Diese PSP stellt sicher, dass alle Pods mit den optimierten Sicherheitseinstellungen ausgeführt werden. Allerdings bestehen natürlich auch die in den vorhergehenden Artikeln beschriebenen Herausforderungen. Denn viele Anwendungen können nicht ohne weiteres mit diesen Einstellungen ausgeführt werden: Beispielsweise weil sie nur als User „root“ starten können, weil sie ins Dateisystem schreiben oder bestimmte Linux Capabilities benötigen. Bei eigenen Anwendungen sind solche Themen häufig schnell gelöst (Security Context – Part 1: Good Practices). Bei Anwendungen von Dritten, die im Cluster betrieben werden sollen, kann dies herausfordernder sein. Das trifft auf viele Standard-Images aus DockerHub wie beispielsweise „mongo“, „postgres“ oder „nginx“ zu. Eine Lösung kann sein ein vertrauenswürdiges, in Hinblick auf Sicherheit optimiertes Image zu verwenden. Im konkreten Fall von „nginx“ bietet der Hersteller sogar eines an: „nginx-unprivileged“. Die Firma Bitnami bietet für viele bekannte Open Source Produkte solche Images Open Source an. Diese werden außerdem häufig in den Paketen (Charts) des Kubernetes Package Manager Helm verwendet. Eine weitere Alternative ist es, selbst ein Image zu erstellen, dass den eigenen Ansprüchen entspricht. Hier steigt allerdings der Aufwand.

Die Verwendung von Helm Charts bringt häufig ihre eigenen Herausforderungen mit sich. Zwei Beispiele: Eine PSP entfernt standardmäßig alle Capabilities. Wenn nun ein Helm Chart eine bestimmte Capability benötigt, müsste sie im SecurityContext des betroffenen Containers hinzugefügt werden. Damit dies durch die Abstraktion des Helm Charts möglich ist, muss das Chart die Option der Änderung des SecurityContext durch die „values.yaml“ bereitstellen. Genauso verhält es sich, wenn für read-only File-Systems ein Volume und Volume Mount ergänzt werden muss. Wenn solche Optionen fehlen, können Helm Charts allerdings selbst erweitert werden. Sie sind Open Source und im Kern Kubernetes YAML-Dateien, sodass diese einfach per Pull Request an die eigenen Bedürfnisse anpassbar sind. Bis zur Annahme des Pull Requests kann mit einer lokalen Kopie des Charts gearbeitet werden. Nach Annahme des Pull Requests ist ein Wechsel zurück auf das remote Chart möglich. Der ganze Prozess ist kein Ding der Unmöglichkeit, verursacht aber Zusatzaufwand.

Je nachdem wie das Image gebaut ist und ob Pull Requests akzeptiert werden, kann es sein, dass diese Lösungsvorschläge nicht oder nur mit sehr hohem Aufwand zum Erfolg führen. In diesem Fall gibt es eine weitere Lösung: die Ausführung des Pods mit weniger strengen Sicherheitseinstellungen. Diese werden in weiteren PSPs beschrieben.

Freizügigere PSP pro Pod definieren

Falls ein Pod oder genauer dessen Service Account, für die Verwendung von mehreren PSPs berechtigt ist, geht der PSP Admission Controller wie folgt vor: Die erste der nach Namen sortierten PSPs, die den Pod erlaubt, wird verwendet (Kubernetes Docs: Pod Security Policies). Hier zeigt sich, dass es möglich ist eine Cluster-weit für alle Pods aktivierte, einschränkende PSP durch freizügigere PSPs für einzelne Pods zu übersteuern.

Doch wie wird eine solche PSP erstellt und nur für einzelne Pods aktiviert? Zunächst wird natürlich die PSP selbst benötigt. Hier empfiehlt es sich die einschränkende PSP (siehe beispielsweise Pod Security Policies Teil 1) zu duplizieren und den oder die Werte die freizügiger sein sollen entsprechend anzupassen.

Diese PSP wird dann wie gewohnt per Role Based Access Control (RBAC) aktiviert. Allerdings wird die Verwendung dieser PSP nicht wie im vorherigen Artikel für alle Service Accounts erlaubt, sondern nur für einen bestimmten. Hier empfiehlt sich dann auch die Begrenzung auf einen Namespace, durch Verwendung von „Role“ und „RoleBinding“ statt „ClusterRole“ und „ClusterRoleBinding“. Damit ein Pod die PSP verwenden darf, wird diesem noch der Service Account zugewiesen.

Es ist empfehlenswert die Umsetzung mittels RBAC in YAML durchzuführen, damit es unter Versionsverwaltung gestellt werden kann. Erfahrungsgemäß geht es am schnellsten das YAML zu generieren. Listing 1 zeigt wie eine PSP mit Namen „privileged“ einem Service Account mit dem Namen „privileged“ im Namespace „default“ zugewiesen wird. Vorsicht: Beim „RoleBinding“ muss der Namespace zweimal angegeben werden, einmal für die „RoleBinding“ Ressource und einmal bei der Referenzierung des Service Accounts. Es ist auch möglich eine Cluster-weit definierte Rolle („ClusterRole“) in mehreren Namespaces zu verwenden. Dazu würde beim Erzeugen des „rolebinding“ entsprechend „--clusterrole“ verwendet. Dadurch entfällt die redundante Pflege von freizügigeren PSPs, die in mehreren Namespaces eingesetzt werden. Listing 2 zeigt die YAML-Repräsentation.

kubectl create serviceaccount privileged \
   --namespace=default \
   --dry-run -o yaml

kubectl create role psp:privileged \
    --verb=use \
    --resource=podsecuritypolicy \
    --resource-name=privileged \
    --namespace=default \
    --dry-run -o yaml

kubectl create rolebinding psp:privileged \
    --role=psp:privileged \
    --serviceaccount default:privileged \
    --namespace=default \
    --dry-run -o yaml

Listing 1: Skript zum Generieren von YAML, das PSP per RBAC für Service Account in Namespace aktiviert

apiVersion: v1
kind: ServiceAccount
metadata:
  name: privileged
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: psp:privileged
rules:
  - resourceNames:
      - privileged
    resources:
      - podsecuritypolicies
    verbs:
      - use
    apiGroups:
      - extensions
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: psp:privileged
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: psp:privileged
subjects:
  - kind: ServiceAccount
    name: privileged
    namespace: default

Listing 2: Role und RoleBinding, die Nutzung von PSP erlauben

Zuletzt erfolgt die Zuweisung des berechtigten Service Accounts an einen Pod. Die Verbindung kann direkt im Pod (typischerweise mittels Template in einem übergeordneten Controller wie Deployment, StatefulSet, etc.) mittels des Feldes „serviceAccountName“ hergestellt werden. Listing 3 zeigt ein Beispiel. Ohne das Feld startet ein Pod mit dem Service Account „default“, der in jedem Namespace existiert.

apiVersion: v1
kind: Pod
# ...
spec:
  serviceAccountName: privileged
  containers:
    # ...

Listing 3: Verknüpfung eines Pods mit einem Service Account

Wenn die im letzten Artikel gezeigte PSP im Einsatz ist, ist der Pod jetzt sowohl für die PSPs „restricted“ als auch „privileged“ autorisiert. Somit wird der Pod zugelassen solange er den Einstellungen von „privileged“ entspricht, auch wenn er denen von „restricted“ widerspricht. Entspricht der Pod beiden PSPs nicht, wird er abgelehnt. Damit wurde für diesen Pod eine Ausnahmeregelung mit mehr Rechten, mittels freizügiger PSP implementiert.

Test und Debugging

In der Theorie klingt die Verwendung von PSPs zwar aufwändiger als Einstellungen im Security Context aber dennoch überschaubar. In der Praxis zeigt sich jedoch, dass durch das Zusammenspiel von so vielen Ressourcen die Antworten auf Fragen erstaunlich schwer zu finden sein können: Warum wird ein Pod nicht angelegt oder gestartet? Woran liegt das? Warum sind die Einstellungen nicht wie gewünscht? Warum wird nicht die gewünschte PSP verwendet? Die Antworten auf diese und ähnliche Fragen können in PSP, (Cluster-)Role, (Cluster-)RoleBinding, Service Account, Pod und gegebenenfalls zusätzlich Pod Controller verteilt sein.

Bevor mit der eigentlichen Fehlersuche begonnen wird, empfiehlt es sich die die bereits im letzten Artikel erwähnten Fallstricke zu prüfen:

  • Der PSP Admission Controller muss beim API-Server aktiviert sein, sonst werden PSPs nicht ausgewertet.
  • Dieser Admission Controller verarbeitet Pods zu dem Zeitpunkt an dem ihre Ausführung beim API-Server angefragt wird. Bereits laufende Pods werden nicht erneut geprüft. Falls also der Admission Controller erst später aktiviert, eine PSP erstellt, neu berechtigt oder geändert wird, findet keine erneute Auswertung der PSP für laufende Pods statt.
  • Die in den (Cluster-)Roles angegebene API-Gruppe passt zur verwendeten K8s Version:
    • Versionen kleiner 1.16: „apiGroups: [extensions]“
    • Versionen ab 1.16: „apiGroups: [policy]“

Wenn diese Voraussetzungen erfüllt sind, das Problem aber dennoch besteht, kann die eigentliche Fehlersuche beginnen. Wer eine grafische Darstellung bevorzugt findet diese online (Troubleshooting Kubernetes PodSecurityPolicies ).

+++Infografik zum Troubleshooting von Pod Security Policies: Infografik herunterladen+++
  • Nachdem alle Ressourcen dem Cluster übergeben und laufende Pods gegebenenfalls gelöscht wurden, liegt es nahe zunächst zu prüfen, ob die eigentlichen Pods überhaupt angelegt und gestartet wurden: „kubectl get pods“
  • Wie erwähnt werden Pods meist durch einen Controller verwaltet. Beispiele für Controller sind Deployment, StatefulSet oder CronJob. Wenn die gewünschten Pods nicht angezeigt werden, wird die Fehlersuche beim Controller fortgesetzt. Wenn die Pods nicht angelegt werden konnten, wird dies als Warnung in den Events des Controllers angezeigt, die wie folgt abgefragt werden können: „kubectl describe <controller>“
  • Bei Deployments ist zu beachten, dass die Pods durch ReplicaSets verwaltet werden. Daher wird die Fehlermeldung auch im ReplicaSet angezeigt und nicht im Deployment selbst. Die Ursache für einen Fehler an dieser Stelle kann sein, dass der Service Account des Pods für keine PSP autorisiert ist oder die Sicherheitseinstellungen des Pods keiner der autorisierten PSPs entsprechen. Beispiel: Im Security Context wurden explizit Capabilities angefordert, die von keiner PSP erlaubt werden.
  • Sind die Pods angelegt aber starten nicht, liegt das wahrscheinlich daran, dass die Pods nicht mit den durch die PSP vorgegebenen Sicherheitseinstellungen ausgeführt werden können. Hier sollte zunächst geklärt werden welche PSP überhaupt auf den Pod angewendet wird: „kubectl get pod <pod> -o jsonpath='{.metadata.annotations.kubernetes\.io/psp}'“. Wenn nicht die gewünschte PSP angewendet wurde, hilft die Prüfung, ob der Pod für die PSP berechtigt ist. Dazu ist folgendes Vorgehen denkbar:
    • Wurde der richtige Service Account zugeordnet? Wenn nein, korrigieren. „kubectl get pod <pod> -o jsonpath='{.spec.serviceAccountName}'“
    • Sonst: Ist der Service Account für die gewünschte PSP autorisiert? „kubectl auth can-i use psp/<psp> --as=system:serviceaccount:<namespace>:<serviceAccount>“ Dies prüft, ob die RBAC Einstellungen wie erwartet sind, also ob die Verbindung zwischen (Cluster-) Role, (Cluster-) RoleBinding und Service Account stimmt (s. Abb. 1) . Die Kopplung zwischen diesen Ressourcen ist lose. Das heißt es wird beim Anlegen nicht geprüft, ob beispielsweise ein Service Account oder eine Role überhaupt existieren. Hier reicht also ein Schreibfehler und die Rechte werden nicht wie erwartet zugeordnet. Dieser Schreibfehler wird dann frühestens beim Starten des Pods erkannt. Allerdings ist der Zusammenhang zwischen Fehlermeldung und Schreibfehler im RoleBinding nicht naheliegend. Hier kann eine Übersicht darüber helfen, wem welche Rollen zugeordnet werden. Eine übersichtliche Darstellung bietet das Kubectl Plugin RBAC Lookup. Nach der Installation bietet „kubectl rbac-lookup“ eine Tabelle deren Einträge Subjekte (beispielsweise Service Account), Scope (Name des Namespaces oder Cluster-weit) und Rolle zeigen. Die andere Blickrichtung, wer Zugriff auf welche PSP hat, kann mittels des Kubectl Plugins who-can eingenommen werden. Hier lautet die Syntax: „kubectl who-can use psp <psp>“. Wenn der Service Account für die PSP autorisiert ist, diese aber nicht verwendet wird, deutet dies darauf hin, dass noch weitere PSPs autorisiert sind. Hier gilt es zu prüfen, ob und wie dies mit der oben beschriebenen PSP Reihenfolge des Admission Controllers funktionieren kann.
  • Wenn die gewünschte PSP angewendet wurde und der Container im Status „CrashLoopBackOff“ ist, hilft ein Blick in die Logs: „kubectl logs <pod>“ Hier sind dann Hinweise auf fehlende Rechte, Capabilities, etc. zu finden. Ist der Status „CreateContainerConfigError“, hilft „kubectl describe <pod>“. Hier können wieder die Warnung in den Events Aufschluß geben, beispielsweise wenn die Regel „runAsNonRoot“ aktiv ist aber ein Container als User „Root“ gestartet werden soll. In beiden Fällen gilt es Einstellungen so zu verändern, dass der Pod von der PSP erlaubt wird. Beispielsweise kann die User-ID mittels SecurityContext gesetzt, Volumes gemountet (um temporäre Verzeichnisse bei read-only File-System schreibbar zu machen) oder ein anderes Image gewählt werden, das ohne Capabilities starten kann. Eine Alternativ ist die oben beschriebene Verwendung einer freizügigeren PSP verwendet werden.

Aktivierung von PSPs mittels RBAC

Wenn die Pods dann gestartet sind, macht eine abschließende manuelle Prüfung der effektiven Einstellungen Sinn. Listing 4 zeigt die Abfrage einiger der im Container aktiven Werte. Dies setzt allerdings einen Container voraus, in dem eine Shell vorhanden ist. Wenn der Anwendungscontainer ohne Shell ausgeliefert wird (was aus Sicherheitsgründen empfehlenswert ist) kann ein temporärer Pod Abhilfe schaffen. In diesem können Befehle aus Listing 4 ohne „kubectl exec“ interaktiv ausgeführt werden. Dieser Pod kann auch mit einem bestimmten Service Account gestartet werden (Kubectl Version 1.18): „kubectl run tmp --rm -ti --image debian:buster --overrides='{"spec": {"serviceAccountName": "privileged"}}' -- /bin/bash“

# Aktive Capabilities im Container abfragen:
capsh --decode="$(kubectl exec <pod> cat /proc/1/status | grep CapBnd| cut -d':' -f2)"
# Wo “capsh” nicht verfügbar ist (z.B. Windows, Mac) kann mittels Docker abgeholfen werden:
docker run --rm --entrypoint capsh debian:buster  \
  --decode="$(kubectl exec <pod>-- cat /proc/1/status | grep CapBnd| cut -d':' -f2)"

# Prüfung, ob seccomp aktiv ("2" heißt aktiv)
kubectl exec <pod> cat /proc/1/status | grep Seccomp

# Prüfung, ob Privilege Escalation möglich
kubectl exec <pod> -- cat /proc/1/status | grep NoNewPrivs

# Einfacher Test zum Schreiben in ein read-only Filesystem
kubectl exec <pod> -- touch a
# touch: cannot touch 'a': Read-only file system

# Environment eines Containers abfragen
kubectl exec <pod> -- env

# User eines Containers abfragen
kubectl exec <pod> -- id

Listing 4: Effektive Einstellungen im Container prüfen

Empfehlung zum Einsatz von PSPs

Zusammenfassend kann festgehalten werden, dass der Einsatz von PSPs definitiv sinnvoll ist, da sie zur Erhöhung der Sicherheit im Cluster beitragen. Denn bei Kubernetes wurden viele Standardwerte eher im Hinblick auf Kompatibilität als auf Sicherheit gewählt. Dies kann durch PSPs an vielen Stellen effektiv optimiert werden. Beim Aufsetzen eines neuen Clusters ist es empfehlenswert direkt mit einer restriktiven PSP zu beginnen. Schwieriger ist es bei einem produktiven Cluster nachträglich eine PSP einzuführen. Auch hier wird diese die Sicherheit optimieren, es wird allerdings auch zusätzlicher Aufwand entstehen. Hier bietet es sich an, die Einstellungen zunächst sukzessive bei allen Pods im Cluster mittels des Security Context auszuprobieren. Dabei kann geprüft werden, ob und wie Pods mit den Einstellungen ausführbar sind. Wenn das mit allen Pods funktioniert kann die PSP mit geringerem Risiko und Aufwand eingeführt und die Sicherheit Cluster-weit auch für zukünftige Anwendungen verbessert werden.

Fazit

Der vorhergehende Artikel beschreibt, wie die Standardeinstellung von Kubernetes mittels PSPs sicherer, weil einschränkender, gestaltet werden können. Zu den dadurch entstehenden Herausforderungen zeigt dieser Artikel Lösungen auf. In der Praxis kann es vorkommen, dass Images nicht mit überschaubarem Aufwand mit den durch eine Cluster-weit gültige PSP vorgegebenen Einstellungen ausgeführt werden können. Pods können dann von der allgemein gültigen PSP ausgenommen werden. Realisierbar ist dies durch weitere, weniger strikte PSPs, für die einzelne Pods bei Bedarf autorisiert werden. Da PSPs mittels RBAC autorisiert werden, müssen für den erfolgreichen Einsatz viele lose gekoppelte Kubernetes Ressourcen zusammenspielen: PSP, (Cluster-)Role, (Cluster-)RoleBinding, Service Account, Pod und gegebenenfalls zusätzlich Pod Controller. Daher kann die Fehlersuche herausfordernd sein. Hier bietet dieser Artikel einen Leitfaden zum Finden von Fehlern. Trotz allem empfiehlt dieser Artikel die Verwendung von PSPs. Insbesondere wenn diese von Anfang an eingesetzt werden, erhöhen sie mit überschaubarem Aufwand die allgemeine Sicherheit aller Anwendungen im Cluster. Zum Abschluss dieser Artikelserie möchte ich mich bei meinen Kollegen von der Cloudogu GmbH bedanken. Zum einen bei allen Kollegen vom Marketing für die Unterstützung bei Organisatorischem, Korrekturlesen, Erstellen von Grafiken und Übersetzen. Zum anderen geht mein besonderer Dank an Sebastian Sdorra für die vielen konstruktiven inhaltlichen Vorschläge zu jedem einzelnen Artikel.


Johannes Schnatterer
Johannes Schnatterer

- Solution Architect -

Johannes ist Continuous Delivery Enthusiast, fokussiert auf Software Qualität, hat einen ausgeprägten Open-Source-Enthusiasmus und ist überzeugt, dass prägnante Dokumentation entscheidend sein kann.