Skip to main content

Kubernetes securityContext: Linux-Funktionen in Kubernetes

Artikel von
Kubernetes securityContext

26. Januar 2021

0 Min. Lesezeit

In grauer Vorzeit hatten Unix-Betriebssysteme ein relativ einfaches Berechtigungsmodell. Entweder waren Sie ein normaler Benutzer oder root – der Superuser mit allen Berechtigungen.

Normalen Benutzern konnten zwar erweiterte Berechtigungen für Dateien oder Verzeichnisse erteilt werden, doch fast alle Funktionen auf Kernel-Ebene waren dem root-Benutzer vorbehalten. Benötigte Ihre Anwendung einen einzigen Kernel-Aufruf, um zu funktionieren, musste sie mit SUID-Berechtigungen ausgestattet werden – dadurch erhielt der Prozess effektiv root-Berechtigungen, wenn er von einem normalen Benutzer gestartet wurde.

Das funktionierte in einer Zeit, in der physische Rechner von einer relativ kleinen Benutzergruppe verwendet wurden, recht gut. Diese grobe Abstufung war für die moderne Welt jedoch nicht wirklich geeignet. Um mehr Flexibilität und Sicherheit zu ermöglichen, entwickelten die Linux-Kernel-Entwickler eine deutlich feinere Lösung: Sie gruppierten Kernel-Aufrufe in Funktionen und erlaubten, diese einem einzelnen Prozess zuzuweisen. Benötigte Ihre Anwendung nun einen einzelnen Kernel-Aufruf, konnte ihr genau diese Funktion zugewiesen werden – und so die Sicherheitsrisiken für Ihr System begrenzen.

Berechtigungen in Kubernetes-Containern verwalten

Wie funktioniert das also in Containern?

Ein Container ist im Grunde ein Prozess, der auf dem System ausgeführt und mithilfe von cgroups und Namespaces im Kernel isoliert wird. Daher lassen sich einem Container Funktionen genauso zuweisen wie jedem anderen Prozess. Das übernimmt die Container-Runtime beim Erstellen des Containers.

Üblicherweise weist die Container-Runtime dem Container eine Reihe von Standardfunktionen zu (die von Docker bereitgestellte Standardauswahl finden Sie hier) und bietet außerdem die Möglichkeit, Funktionen hinzuzufügen oder zu entfernen. Starten Sie einen Container mit dem Flag --privileged, weisen Sie ihm alle Funktionen zu. Wenn der Container als root ausgeführt wird, kann dadurch ein ernstes Sicherheitsproblem entstehen – ein Thema, das ich in einem früheren Beitrag behandelt habe.

Funktionen für einen Container im Kubernetes-securityContext festlegen

Bei der Verwendung der Container-Runtime in Kubernetes nutzt die Kubernetes-Steuerungsebene diese Einstellungen, um festzulegen, mit welchen Funktionen unser Container gestartet werden soll. Die Konfiguration dieser Funktionen wird über verschiedene Einstellungen im Abschnitt securityContext der YAML-Datei eines Containers zugänglich gemacht. Sie sieht folgendermaßen aus:

securityContext:
      capabilities:
        drop:
          - ALL
        add: [“NET_ADMIN”]

In diesem Fall würden wir alle Funktionen entfernen und anschließend die Funktion CAP_NET_ADMIN hinzufügen. Ist der Abschnitt capabilities im securityContext leer, erhalten wir die von der Container-Runtime festgelegte Standardauswahl an Funktionen. Diese ist üblicherweise recht großzügig bemessen und umfasst möglicherweise deutlich mehr Funktionen, als unsere Anwendung benötigt. Starten wir einen Pod in Kubernetes und sehen uns an, welche Funktionen er erhält.

Zunächst erstellen wir eine YAML-Datei, um einen Pod bereitzustellen. In dieser Konfiguration verwenden wir ein Upstream-Image von Docker Hub. Das auf Alpine basierende Image enthält das Tool capsh, mit dem wir die Funktionen unseres Containers anzeigen können.

Beachten Sie, dass wir für diesen Container überhaupt keinen securityContext-Abschnitt definiert haben. Daher gelten für alle Einstellungen die Systemstandards. Denken Sie daran: Wenn in Kubernetes kein Benutzer angegeben ist, wird der Container als der Standardbenutzer ausgeführt, der in der Dockerfile festgelegt ist, aus der er erstellt wurde. Bei vielen Containern ist das root.

% cat caps.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: caps
  labels:
    app: caps
spec:
  containers:
  - name: caps
    image: ollijanatuinen/capsh
    command: ["/bin/sleep", "3650d"]
    imagePullPolicy: IfNotPresent
  restartPolicy: Always

% kubectl apply -f caps.yaml 
pod/caps created

Sobald der Pod ausgeführt wird, können wir eine Shell darin öffnen und mit capsh die Funktionen überprüfen:

% kubectl exec --stdin --tty caps -- ash 
/ # capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+eip
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Funktionen im Kubernetes-securityContext entfernen

Wie wir sehen, wird der Container standardmäßig als root ausgeführt und verfügt über zahlreiche Funktionen. Diese werden standardmäßig von der Container-Runtime festgelegt. Versuchen wir nun, eine dieser Funktionen über die Einstellungen im securityContext zu entfernen: Wir entziehen dem Container die Möglichkeit, neue Dateisystemknoten anzulegen. Dafür ist CAP_MKNOD zuständig:

matt@Mattbook capabilities_testing % cat dropcaps.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: dropcaps
  labels:
    app: dropcaps
spec:
  containers:
  - name: dropcaps
    image: ollijanatuinen/capsh
    command: ["/bin/sleep", "3650d"]
    imagePullPolicy: IfNotPresent
    securityContext:
      capabilities:
        drop: ["MKNOD"]
  restartPolicy: Always

% kubectl apply -f dropcaps.yaml 
pod/dropcaps created

% kubectl exec --stdin --tty dropcaps -- ash
/ # capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_audit_write,cap_setfcap+eip
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_audit_write,cap_setfcap
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Im Vergleich zum ersten Pod sehen wir, dass bei diesem Pod nun die Funktion cap_mknod entfernt wurde. Neben einzelnen Funktionen können wir über den securityContext auch alle Funktionen entfernen:

matt@Mattbook capabilities_testing % cat nocaps.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: nocaps
  labels:
    app: nocaps
spec:
  containers:
  - name: nocaps
    image: ollijanatuinen/capsh
    command: ["/bin/sleep", "3650d"]
    imagePullPolicy: IfNotPresent
    securityContext:
      capabilities:
        drop:
          - ALL
  restartPolicy: Always

matt@Mattbook capabilities_testing % kubectl apply -f nocaps.yaml 
pod/nocaps created

matt@Mattbook capabilities_testing % kubectl exec --stdin --tty nocaps -- ash
/ # capsh --print
Current: =
Bounding set =
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Ohne jegliche Funktionen schlagen bestimmte Systemaktionen fehl. Versuchen wir beispielsweise, bash mit apk zu installieren:

/ # apk add bash
(1/5) Installing ncurses-terminfo-base (6.1_p20180818-r1)
(2/5) Installing ncurses-terminfo (6.1_p20180818-r1)
(3/5) Installing ncurses-libs (6.1_p20180818-r1)
(4/5) Installing readline (7.0.003-r0)
(5/5) Installing bash (4.4.19-r1)
Executing bash-4.4.19-r1.post-install
ERROR: bash-4.4.19-r1.post-install: script exited with error 127
Executing busybox-1.28.4-r1.trigger
ERROR: busybox-1.28.4-r1.trigger: script exited with error 127
1 error; 13 MiB in 19 packages

Obwohl unser Container als root ausgeführt wird, schlägt die Installation des bash-Pakets fehl, da dafür Dateisystemberechtigungen festgelegt werden müssen. Diese Funktion haben wir dem Container entzogen. Wenn wir verhindern möchten, dass Software in unserem Container installiert wird, können wir die Berechtigungen also über Funktionen verwalten.

capsh bietet zwar eine übersichtliche Darstellung der Funktionen unseres Containers, ist aber nicht die einzige Möglichkeit, diese Informationen abzurufen. Wir können direkt auf das proc-Dateisystem zugreifen, ohne zusätzliche Software zu installieren:

/ # cd /proc/1/
/proc/1 # cat status 
----truncated
CapPrm:0000000000000000
CapEff:0000000000000000
CapBnd:0000000000000000
CapAmb:0000000000000000
----truncated

In der Datei /proc/1/status werden die Funktionen als Bitmaske angezeigt. Da in diesem Container keine Funktionen aktiviert sind, bestehen die Werte nur aus Nullen. Sehen wir uns dieselbe Datei im ursprünglichen Container an, in dem alle Funktionen aktiviert sind:

/ # cd /proc/1/
/proc/1 # cat status 
----truncated
CapInh:00000000a80425fb
CapPrm:00000000a80425fb
CapEff:00000000a80425fb
CapBnd:00000000a80425fb
----truncated

Das ist nicht ganz so gut lesbar wie die Ausgabe von capsh. Jedes hier angezeigte Bit steht jedoch für eine bestimmte Linux-Capability, die im entsprechenden Kernel-Header definiert ist.

Erfahren Sie, wie Sie die Kubernetes-Sicherheit verbessern können, indem Sie Standardfunktionen für einen Container entfernen.

Linux-Capabilities im Kubernetes-securityContext hinzufügen

Neben dem Entfernen von Linux-Capabilities können wir sie auch wieder hinzufügen. Nehmen wir unser vorheriges Beispiel und fügen wir den Einstellungen im securityContext eine einzelne Capability hinzu: CAP_MKNOD.

% cat addcaps.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: addcaps
  labels:
    app: addcaps
spec:
  containers:
  - name: addcaps
    image: ollijanatuinen/capsh
    command: ["/bin/sleep", "3650d"]
    imagePullPolicy: IfNotPresent
    securityContext:
      capabilities:
        drop:
          - ALL
        add: ["MKNOD"]
  restartPolicy: Always

% kubectl create -f addcaps.yaml           
pod/addcaps created
% kubectl exec --stdin --tty addcaps -- ash
/ # capsh --print
Current: = cap_mknod+eip
Bounding set =cap_mknod
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Wie dieses Beispiel zeigt, ist nun nur diese eine Funktion aktiviert.

Das Prinzip der geringsten Berechtigungen im Kubernetes-securityContext

In diesem Beitrag haben wir gesehen, wie Funktionen in Containern arbeiten, wie sie im Kubernetes-securityContext konfiguriert und von der Container-Runtime gesteuert werden.

Gemäß dem Prinzip der geringsten Berechtigungen sollten aus Sicherheitssicht nur die Funktionen bereitgestellt werden, die unser Container tatsächlich benötigt. Dieses Thema habe ich in einem früheren Beitrag behandelt. Dabei mag es überraschen, dass die meisten Prozesse überhaupt keine Kernel-Funktionen benötigen. Denn selbst wenn sie erweiterte Berechtigungen erfordern, lassen sich diese meist über Berechtigungen auf Dateiebene steuern.

Entfernen Sie zunächst alle Funktionen im securityContext und fügen Sie anschließend nur die benötigten hinzu. Fehler können Sie beheben, indem Sie die Ausgabe von Tools wie SELinux prüfen, um herauszufinden, welche Funktionen möglicherweise die Ursache sind. Denken Sie auch daran, dass Kubernetes-Container möglicherweise als root ausgeführt werden, sofern Sie keinen anderen Benutzer festlegen.

Einstellungen für Kubernetes-securityContext-Funktionen durchsetzen

Wenn Sie sicherstellen möchten, dass securityContext-Einstellungen wie Funktionen und die Ausführung als Nicht-root festgelegt sind, können Sie Admission Controller in Ihrem Kubernetes-Cluster verwenden. Damit stellen Sie sicher, dass Container nur mit den richtigen Sicherheitseinstellungen gestartet werden. Kubernetes verfügt über den integrierten PodSecurityPolicy-Controller, mit dem sich securityContext-Einstellungen durchsetzen lassen.

Beachten Sie jedoch, dass diese Funktion mit Version 1.21 veraltet sein wird. Stattdessen kommen extern gepflegte Projekte wie Open Policy Agent zum Einsatz.

Wir sollten außerdem Transparenz und die Behebung solcher Sicherheitsprobleme direkt in unseren Entwicklungsprozess integrieren. Snyk kann Ihre Kubernetes-YAML-Dateien scannen, unsichere securityContext-Einstellungen für Funktionen und andere Konfigurationen erkennen und direkt in Entwickler-Workflows Empfehlungen zur Behebung bereitstellen. Diese Funktion ist über die Snyk CLI verfügbar und lässt sich außerdem direkt in Quellcodeverwaltungssysteme und Continuous-Integration-Systeme integrieren:

% snyk iac test caps.yaml

Testing caps.yaml...

Infrastructure as code issues:
  ✗ Container is running with default set of capabilities [Medium Severity] [SNYK-CC-K8S-6] in Deployment
    introduced by input > spec > containers[caps] > securityContext > capabilities > drop

  ✗ Container is running without root user control [Medium Severity] [SNYK-CC-K8S-10] in Deployment
    introduced by input > spec > containers[caps] > securityContext > runAsNonRoot

  ✗ Container is running without memory limit [Low Severity] [SNYK-CC-K8S-4] in Deployment
    introduced by input > spec > containers[caps] > resources > limits > memory

  ✗ Container is running without cpu limit [Low Severity] [SNYK-CC-K8S-5] in Deployment
    introduced by input > spec > containers[caps] > resources > limits > cpu

  ✗ Container is running with writable root filesystem [Low Severity] [SNYK-CC-K8S-8] in Deployment
    introduced by input > spec > containers[caps] > securityContext > readOnlyRootFilesystem

  ✗ Container is running without AppArmor profile [Low Severity] [SNYK-CC-K8S-32] in Deployment
    introduced by metadata > annotations['container.apparmor.security.beta.kubernetes.io/caps']

  ✗ Container is running without liveness probe [Low Severity] [SNYK-CC-K8S-41] in Deployment
    introduced by spec > containers[caps] > livenessProbe

  ✗ Container could be running with outdated image [Low Severity] [SNYK-CC-K8S-42] in Deployment
    introduced by spec > containers[caps] > imagePullPolicy

Organization:      matt-jarvis-snyk
Type:              Kubernetes
Target file:       caps.yaml
Project name:      capabilities_testing
Open source:       no
Project path:      caps.yaml

Tested caps.yaml for known issues, found 8 issues
Kubernetes-Sicherheitskontext: Codebeispiel mit einem Container, der mit privileged: true und den Standardfunktionen konfiguriert ist

Möchten Sie diese Funktion selbst ausprobieren? Erstellen Sie noch heute ein kostenloses Konto!

Starten Sie mit Capture the Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.