Blogartikel

30.09.2020

DevOps

Kubernetes AppOps Security Teil 5: Pod Security Policies (1/2) – Good Practices

Pod Security Policies (PSP) ermöglichen es Cluster-weit Einstellungen vorzunehmen, die für alle neuen Container gelten. Im Vergleich zum Security Context ist die Verwendung von PSPs aufwändiger. Besonders für große Organisationen mit großen Clustern zahlt es sich aber aus, da Container weniger manuell konfiguriert werden müssen.

+++ Wichtig:"Pod Security Policies" sind seit Kubernetes Version 1.21 deprecated und werden mit Kubernetes Version 1.25 komplett entfernt werden. Ihr Nachfolger sind "Pod Security Standards".+++

Die vorhergehenden beiden Artikel dieser Serie (Security Context Teil 1, Security Context Teil 2) zeigen Einstellungen in Kubernetes, deren Standardwerte hinsichtlich Sicherheit verbesserungswürdig sind. Die Artikel zeigen wie diese Einstellungen in Kubernetes mittels des Security Context optimiert werden können. Dieser Security Context wird generell pro Container oder Pod angegeben. Die Benutzung des Security Context kann in der Praxis durchaus Sinn machen: In kleineren Teams ist es einfach möglich sich darauf zu einigen, welche Optionen gesetzt werden und diese dann sukzessive auf alle Anwendungen auszurollen. Auch ein Test, ob Anwendungen mit den restriktiveren Sicherheitseinstellungen noch funktionieren, lässt sich mit dem Security Context leicht durchführen. Allerdings gibt es auch Anwendungsfälle, in denen es Sinn macht, Cluster-weit Einstellungen zu verändern: Beispielsweise wenn in einem neuen Cluster alle zukünftigen Anwendungen von Anfang an mit möglichst wenig Rechten starten sollen (Least Privilege). In größeren Organisationen beziehungsweise bei einem größeren Personenkreis mit Zugriff auf den Cluster, lässt sich damit sicherstellen, dass sich alle an die vorgegebene Sicherheitsrichtlinie halten. Beides lässt sich in Kubernetes mittels Pod Security Policies (PSPs) realisieren.

Wie PSPs verwendet werden, wird im Folgenden anhand von Beispielen gezeigt. Wer dies in einer definierten Umgebung ausprobieren will, findet vollständige Beispiele mit Anleitung im Repository „cloudogu/k8s-security-demos” bei GitHub.

Überblick über die Funktionsweise

Im Gegensatz zum Security Context ist die Verwendung von PSPs aufwändiger. Daher zunächst ein kurzer Überblick. Die einzelnen Punkte werden dann in den nächsten Abschnitten im Detail vorgestellt.

  • In der Kubernetes Ressource “PodSecurityPolicy” werden erlaubte Werte und Standardwerte deklariert.
  • Die Zuordnung zwischen Pod und PSP erfolgt über Role Based Access Control (RBAC). Pods werden wie folgt für die Verwendung von PSP autorisiert.
    • Eine Rolle erlaubt die Verwendung einer PSP (“Role”).
    • Eine Rolle wird an Service Accounts gebunden (“RoleBinding”).
    • Jedem Pod ist ein Service Account zugeordnet, mit dessen Rechten er ausgeführt wird.
  • PSPs werden von einem speziellen Admission Controller durchgesetzt: Jeder schreibende Request an den Kubernetes API-Server wird durch den Admission Controller geleitet. Dieser prüft die im Request enthaltenen Pods gegen die ihm zugeordneten PSPs. Je nach Einstellung werden hier Werte überschrieben oder das Starten des Pods verhindert.

PSP Admission Controller

Der Admission Controller, der die Auswertung der Ressource “PodSecurityPolicy” vornimmt, muss beim Kubernetes API-Server aktiviert werden. Auch bei managed Kubernetes Clusters gibt es meist eine Option zur Aktivierung von PSPs. Der Admission Controller beinhaltet gleich die ersten Fallstricke:

  • Wie der Name Admission Controller schon suggeriert, kontrolliert dieser den Zugang zum API-Server. Wichtig ist, dass bereits laufende Pods nicht erneut geprüft werden, falls der Admission Controller erst später aktiviert oder eine PSP geändert wird.
  • Ist der PSP Admission Controller nicht aktiv, werden “PodSecurityPolicy” Ressourcen im Cluster nicht ausgewertet. Dies kann zu einem falschen Gefühl der Sicherheit führen (Kubernetes Security Audit).
  • Ist der PSP Admission Controller aktiv aber dem Service Account des Pods keine PSP zugeordnet, wird der Pod nicht gestartet.

Letzteres ist der Grund warum der PSP Admission Controller standardmäßig deaktiviert ist. Vor dem Aktivieren an einem laufenden Cluster sollte daher zunächst eine PSP eingespielt und für alle Service Accounts autorisiert werden. Sonst besteht die Gefahr, dass keine neuen Pods mehr gestartet werden können. Wie das geht wird in den folgenden Absätzen beschrieben.

Die “PodSecurityPolicy” Ressource

In der PSP kann eine große Anzahl von Einstellungen eingeschränkt oder gesetzt werden. Bei konkreten Anforderungen zur Festlegung von Cluster-weiten Einstellungen helfen die offizielle Dokumentation und die Beschreibung des API. Die Dokumente enthalten einige Beispiele, die den Einstieg in das Thema erleichtern. Bei den Einstellungen ist leider nicht immer ersichtlich was der Standardwert ist und ob die Einstellungen Werte im Pod prüfen (und den Pod gegebenenfalls ablehnen), setzen oder überschreiben. Konkrete Beispiele für die verschiedenen Verhalten folgen später in diesem Abschnitt. Die Erfahrung zeigt, dass Pods dann abgelehnt werden, wenn sie explizite Einstellungen im Security Context vornehmen, die nicht der PSP entsprechen. Werden keine Werte im Security Context angegeben, werden die Werte aus der PSP gesetzt. Auch die UserID, die durch das Image vorgegeben ist, wird dadurch potenziell überschrieben. Dieses Verhalten entspricht also Cluster-weiten Standardwerten. Die größte Herausforderung bleibt die Auswahl der zu setzenden Werte - wie immer ein Kompromiss zwischen Benutzbarkeit und Sicherheit.

Dieser Artikel zeigt, wie die in den vorherigen Artikeln hergeleitete good Practice auf Basis des Security Context pro Container in eine Cluster-weit gültige PSP transformiert werden kann. Listing 1 zeigt diese PSP (leicht verkürzt aus „cloudogu/k8s-security-demos”). Die erste Zeile des Listings zeigt, dass PSPs im Kubernetes-API noch im Beta-Stadium sind. Dieses Thema wird in einem separaten Abschnitt vertieft.

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted
  annotations:
    seccomp.security.alpha.kubernetes.io/defaultProfileName: runtime/default
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: runtime/default
   # Dasselbe für apparmor.security…
spec:
  runAsUser:
    rule: MustRunAs
    ranges:
      - min: 100000
          max: 999999
  runAsGroup: # wie runAsUser
  supplementalGroups: # wie runAsUser
  fsGroup: # wie runAsUser
  defaultAllowPrivilegeEscalation: false
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  requiredDropCapabilities: [ ALL ]
  allowedCapabilities: []
  privileged: false
  hostIPC: false
  hostPID: false
  hostNetwork: false
  hostPorts: []
  allowedHostPaths: []
  volumes:
    - configMap
    - emptyDir
    - projected
    - secret
    - downwardAPI
    - persistentVolumeClaim
  seLinux:
    rule: RunAsAny

Listing 1: PSP, die Einstellungen aus vorherigen Artikeln (und weitere) Cluster-weit festlegt

  • “defaultProfileName”: In der PSP wird per Annotation das standard Seccomp-Profil der Container Runtime (beispielsweise Docker) aktiviert. Dieses Profil ist in Kubernetes sonst explizit deaktiviert. Das Profil muss zusätzlich erlaubt werden, da sonst mit dieser PSP kein Pod mehr gestartet werden kann (“allowedProfileNames”). Anmerkung: Vor Kubernetes 1.19 haben es Seccomp und AppArmor noch nicht in das offizielle API geschafft. Deshalb werden diese in der PSP, genau wie im Security Context (Security Context Teil 1), noch über eine Annotation festgelegt. Dies ist das übliche Vorgehen für „Alpha“-Features, bevor sie einem bestimmten Kubernetes API-Objekt zugewiesen werden.
  • “runAsUser”, “runAsGroup”: Die PSP sorgt dafür, dass Container mit hohen User- und Group-IDs ausgeführt werden. Bei hohen IDs ist es unwahrscheinlicher, dass sie auf dem Host System vergeben sind. Bestehende User könnten Rechte (beispielsweise auf Dateien) auf dem System haben, die sonst der Container auch hätte. Die Einstellungen sorgen dafür, dass Pods, in deren SecurityContext keine User-ID angegeben ist, mit der User-ID 100000 gestartet werden. Wird dort explizit eine User-ID im zulässigen Bereich festgelegt (hier zwischen 100000 und 999999), wird diese unverändert übernommen. Eine Wahl auf User-IDs außerhalb des Bereichs führt zur Ablehnung des Pods durch den Admission Controller. Diese Regel verhindert damit gleichzeitig, dass Container nicht als User oder Group “root” (User-ID 0) ausgeführt werden. Wer nur dies sicherstellen, und keine weiteren Vorgaben bezüglich der User-ID machen möchte, kann hier alternativ die Regel “MustRunAsNonRoot” verwenden. Diese verändert die User-ID nicht, sondern prüft diese nur. Ist die User-ID 0, wird der Pod abgelehnt.
  • “defaultAllowPrivilegeEscalation”: Damit zur Laufzeit keine “root” Rechte erlangt werden können, wird der Standardwert für das Erlauben von Privilege Escalation verändert. Zudem wird per “allowPrivilegeEscalation” sichergestellt, dass dieser Wert in Pods nicht überschrieben werden kann.
  • “readOnlyRootFilesystem”: Um sicherzustellen, dass Anwendungs-Code zur Laufzeit nicht verändert werden kann, wird Schreiben in das Dateisystem des Containers standardmäßig verhindert. Anders als bei “PrivilegeEscalation” gibt es hier nur den einen Wert, der den Standardwert setzt und gleichzeitig nicht pro Pod verändert werden kann.
  • “requiredDropCapabilities”: Um die Angriffsfläche weiter zu reduzieren, werden die von Container Runtimes standardmäßig vergebenen Linux Capabilities entfernt. Außerdem wird verhindert, dass einzelne Pods bestimmte Capabilities bekommen können (“allowedCapabilities”).

Zusätzlich stellt die PSP sicher, dass Einstellungen, deren Standardwerte sicherheitstechnisch sinnvoll sind, nicht im Security Context von Pods verändert werden können.

  • “apparmor.security…”: Das Setzen dieser Annotationen führt dazu, dass das AppArmor Profil nicht mehr deaktiviert werden kann.
  • “privileged”: Container können nicht mehr als “privileged” gestartet werden. Dies würde die Isolation der Container weitestgehend auflösen und viele Rechte auf dem Host einräumen.
  • “hostIPC”, “hostNetwork”, “hostPID”: Container können nicht mehr in Linux Namespaces des Hosts gestartet werden. Auch dies würde die Isolation der Container verringern.
  • “hostPorts”: Zusätzlich wird sichergestellt, dass ein Container nicht direkt an Ports des Hosts bindet. Dies ist zwar ohnehin nur im Host Network Namespace möglich, also hier eigentlich redundant. Redundanz ist im Bereich Sicherheit allerdings etwas positives, da dies eine weitere Verteidigungsschicht darstellt.
  • “volumes”: Die erlaubten Volume -Typen werden eingeschränkt. Zusätzlich wird verhindert, dass Verzeichnisse direkt aus dem Host eingebunden werden (“allowedHostPaths”). Dies verhindert Zugriffe auf das Dateisystem des Hosts und damit auch auf den Docker Socket. Wird dies nicht eingeschränkt, kann der Socket in Container eingebunden werden. Darüber bekommt der Container “Root”-Rechte auf dem Nodes des Clusters.
  • “seLinux”: Die Einstellung für SELinux werden im Beispiel nicht eingeschränkt, da hier die Verwendung von AppArmor angenommen wird. Da die Option ein Pflichtfeld ist, muss sie trotzdem gesetzt werden. Wer Linux Distributionen betreibt die mit SELinux statt mit AppArmor arbeiten (beispielsweise RedHat-Linux Distributionen) sollte hier tätigt werden. Im Gegensatz zu AppArmor ist SELinux bereits im Kubernetes API enthalten, es muss also nicht über eine Annotation angesprochen werden.

Weitere Informationen zu den einzelnen Punkten können der API-Dokumentation von Kubernetes entnommen werden. Diese enthält außerdem noch weitere Einstellungen, bei denen sich aber kein Wert zur pauschalen Verbesserung der Sicherheit aufdrängt. Hier lohnt es sich zu prüfen, ob für den eigenen Anwendungsfall gegebenenfalls weitere Einstellungen Sinn machen.

Einschränkungen von PSPs

Nicht alle Einstellungen die pro Pod möglich sind, können auch in einer PSP festgelegt werden. Diese müssen also pro Pod gesetzt werden. Alternativ können eigene Admission Controller Webhooks implementiert werden, die Werte in den Pods ändern.

  • Es kann nicht standardmäßig deaktiviert werden, dass in Pods ein Token zur Authentifizierung am API-Server eingebunden wird (“automountServiceAccountToken” im Pod). Viele Anwendungen benötigen dies nicht, aber für Angreifer kann es der erste Schritt zum Zugriff auf die Kubernetes API sein.
  • Auch das Einfügen von Service-Namen und Ports in die Environment Variablen der Container kann nicht standardmäßig deaktiviert werden (“enableServiceLinks” im Pod). Dieser veraltete Mechanismus zur Service Discovery (auch bekannt als “Docker Links”) wird kaum mehr verwendet. Er bietet Angreifern sehr einfach eine Auswahl weiterer Ziele.

Aktivierung per RBAC

Nach der Definition der PSP muss diese aktiviert werden. Dies wird realisiert indem ein Service Account mittels RBAC für die Verwendung einer PSP autorisiert wird. Wird ein Pod mit diesem Service Account gestartet, ermittelt der Admission Controller die zugehörigen PSPs und setzt diese durch. Das Verhalten bei mehreren PSPs pro Service Account wird im nächsten Artikel beschrieben.

Die Zuordnung wird in RBAC entweder pro Namespace (mittels “Role” und “RoleBinding”) oder Cluster-weit (“ClusterRole” und “ClusterRoleBinding”) festgelegt. Auch hier ist eine Spezifikation in YAML möglich und sinnvoll, da diese unter Versionsverwaltung gestellt werden kann. Erfahrungsgemäß geht es aber schneller YAML zu generieren. Listing 2 zeigt wie die in Listing 1 beschriebene PSP Cluster-weit aktiviert werden kann, indem mittels einer in Kubernetes standardmäßig vorhandenen Gruppe alle Service Accounts autorisiert werden die PSP zu verwenden. Solange nicht die Verwendung von weiteren, weniger strikten PSPs im Cluster erlaubt ist, werden auf diese Weise die Werte der PSP für jeden Pod im Cluster vorgegeben. Listing 3 zeigt die YAML-Repräsentation.

kubectl create clusterrole psp:restricted \
    --verb=use \
    --resource=podsecuritypolicy \
    --resource-name=restricted \
    --dry-run -o yaml \
  > role-psp-restricted.yml

kubectl create clusterrolebinding psp:all-serviceaccounts \
    --clusterrole=psp:restricted \
    --group system:serviceaccounts \
    --dry-run -o yaml \
  >> role-psp-restricted.yml

Listing 2: Skript zum Generieren von YAML, das eine PSP per RBAC Cluster-weit aktiviert (Kubernetes 1.15)

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: psp:restricted
rules:
  - resources:
      - podsecuritypolicies
    resourceNames:
      - restricted
    verbs:
      - use
    apiGroups:
      - policy
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: psp:all-serviceaccounts
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: psp:restricted
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: Group
    name: system:serviceaccounts

Listing 3: ClusterRole und ClusterRoleBinding, die eine PSP per RBAC Cluster-weit aktivieren (Kubernetes 1.16+)

Bei den beiden Listings gilt es die Kubernetes Version zu beachten. Was es damit auf sich hat beschreibt der nächste Abschnitt.

Status von PSP im Kubernetes API

Wie in Listing 1 ersichtlich, sind PSPs im Kubernetes-API noch im Beta-Stadium. Bei vielen Produkten lässt ein Beta-Stadium darauf schließen, dass es nicht in Produktion verwendet werden sollte. Bei Kubernetes bedeutet dies jedoch im Wesentlichen, dass sich das API noch ändern kann. Historisch gesehen (vor Kubernetes 1.19) waren viele Kubernetes Ressourcen (beispielsweise Deployments) lange im Beta-Stadium und wurden trotzdem schon in der Breite in Produktion genutzt. PSPs wurden in Kubernetes Version 1.8 in der API-Gruppe “extensions” eingeführt. Seit Kubernetes 1.10 sind sie in der Gruppe “policy” (kubernetes/enhancements. Issue #5). Ab Kubernetes 1.16 ist die mittlerweile als veraltet geltenden “extensions” Gruppe standardmäßig deaktiviert (Kubernetes Issue #70672).

In der Praxis zeigt sich leider, dass die Aktivierung per RBAC bis einschließlich Kubernetes 1.15 nur mit der “extensions” Gruppe funktioniert. Danach funktioniert dann ausschließlich die Gruppe “policy” (siehe “ClusterRole” in Listing 3). Erschwerend kommt hinzu, dass das Generieren (Listing 2) nur bis einschließlich Kubernetes 1.15 funktioniert. Danach tritt ein Fehler auf, der erst in Kubernetes 1.18 behoben ist (kubernetes Issue #85314). Hier ist also die Kubernetes Version beim Aktivieren der PSPs zu beachten. Wer zudem PSPs auf Kubernetes Versionen kleiner 1.16 aktiviert, muss daran denken bei der Migration auf die nächsthöhere Version die (Cluster-)RoleBindings anzupassen. Tipps zum Finden von Fehlern werden im nächsten Artikel beschrieben.

Kurz vor dem Release von Kubernetes 1.18 sieht es nicht so aus, als würde PSPs das Beta-Stadium bald verlassen. Generell zeigt sich, dass PSPs aufwändig in der Benutzung sind und verschiedene Alternativen diskutiert werden (kubernetes/enhancements. Issue #5). Bis auf Weiteres bleiben PSPs trotzdem der einfachste Weg Standardeinstellungen Cluster-weit sicherer zu machen. Immerhin ist der Mechanismus ein Bordmittel von Kubernetes und erfordert keine weitere Infrastruktur. Eine zukünftige Alternative zu PSP könnte der Policy Controller “Gatekeeper” des Projekts Open Policy Agent werden. Dabei handelt es sich um ein Geschwisterprojekt von Kubernetes in der Cloud Native Computing Foundation (CNCF). Wann sich alle Features von PSP mit Gatekeeper umsetzen lassen und ob dies dann weniger aufwändig ist als die Verwendung von PSPs muss sich zeigen.

Fazit

Dieser Artikel zeigt wie die in den vorhergehenden Artikeln beschriebene good Practice für Sicherheitseinstellungen in Kubernetes mittels PSPs Cluster-weit durchgesetzt werden kann. Darüber hinaus zeigt er noch weitere sinnvolle Einstellungen, die mittels PSP möglich sind. Dies alles wird anhand der generellen Funktionsweise von PSPs gezeigt: Definition der PSP-Ressource, dann Cluster-weite Aktivierung per RBAC, was zur Auswertung der PSPs durch den Admission Controller führt. Es ist zu beachten, dass der Admission Controller beim API-Server beziehungsweise beim managed Cluster aktiviert sein muss. Ein weiterer Fallstrick der erwähnt wird ist die Gruppe von PSPs im Kubernetes API. Hier muss unterschieden werden, ob Kubernetes 1.16 oder kleiner im Einsatz ist. Generell sind PSPs im Kubernetes API noch im Beta-Stadium. Trotzdem gibt es derzeit keinen einfacheren Weg in Kubernetes Standardeinstellungen Cluster-weit sicherer zu machen.

Der Artikel beschränkt sich auf den einfachsten Fall: Es gibt nur eine PSP und alle Pods können damit gestartet werden. Generell ist dies empfehlenswert, da dadurch alle Pods mit den optimierten Sicherheitseinstellungen ausgeführt werden. Bei der Ausführung entstehen natürlich auch die in den vorhergehenden Artikeln beschriebenen Herausforderungen. Bei vielen Images bedarf es noch kleinerer Anpassungen, damit aus diesem Container mit den auf Sicherheit optimierten Einstellungen gestartet werden können. In der Praxis können auch Fälle auftreten, in denen beispielsweise nicht selbst geschriebene Anwendungen nicht mit diesen Einstellungen ausführbar sind, weil sie nur mit dem User “root” laufen. Wie einzelne Pods mit einer weniger einschränkenden PSP gestartet werden zeigt der nächste Artikel dieser Serie.

Tags