Kubernetes AppOps Security Teil 4: Security Context (2/2) – Hintergründe & Tipps
Ein Container ist im Grunde ein normaler Linux Prozess ist, der durch bestimmte Kernel-Komponenten isoliert vom Rest des Systems läuft. Dies macht Container leichtgewichtiger, aber auch angreifbarer als virtuelle Maschinen (VMs). Um diese Angriffsfläche zu reduzieren bieten Container Runtimes vielfältige Einstellungen, deren Standardwerte einen Kompromiss zwischen Benutzbarkeit und Sicherheit darstellen. An dieser Stelle kann man als Entwickler durch einige good Practices die Angriffsfläche verkleinern. Dies gilt bei der Benutzung einer Container Runtime wie Docker genauso, wie bei der Verwendung von Container Orchestrators wie Kubernetes, da diese nur von unterliegenden Container Runtimes abstrahieren. Welche Einstellungen in Kubernetes vorhanden sind und wie man damit pragmatische die Sicherheit erhöht, zeigt der vorhergehende Artikel in dieser Serie. Dort wird empfohlen mittels des “securityContext” in Kubernetes folgende Einstellungen pro Container vorzunehmen:
- Container mit unprivilegiertem User ausführen,
- ein read-only root Filesystem verwenden,
- Privilege Escalation verhindern,
- Capabilities einschränken und
- das Seccomp default Profil aktivieren.
Wie diese Einstellungen sich zu Angriffsvektoren auf Container verhalten, wie die Isolation bei Containern funktioniert und wie sich diese von VMs unterscheiden, wird in diesem Artikel besprochen. Abschließend werden Tools vorgestellt und ein Ausblick auf weiterführende sicherheitsrelevante Einstellmöglichkeiten gegeben. Die Auswirkungen der Einstellungen können in einer definierten Umgebung ausprobiert werden. Dazu finden sich vollständige Beispiele mit Anleitung im Repository „cloudogu/k8s-security-demos” bei GitHub.
Isolation bei Containern
Generell bieten die Standardeinstellungen in Container Runtimes eine gewisse Isolation des Prozesses durch die Kernel- Komponenten Namespaces, CGroups und Linux Security Modules (wie Capabilities, seccomp, AppArmor und SELinux). Teilweise überlappen diese Mechanismen, sodass ein Schutz besteht, auch wenn einer der Mechanismen durch eine Schwachstelle oder Fehlkonfiguration überwunden wird. Ein Beispiel aus frazelle-container-security: Der mount
Syscall wird durch die default AppArmor und Seccomp Profile, sowie die Capability CAP_SYS_ADMIN
verhindert.
Über diese Standardeinstellungen hinaus bieten Container Runtimes Sicherheitseinstellungen, die auch beim Betrieb auf VMs sinnvoll wären, wie ein read-only root Filesystem oder das generelle Ausschließen von Privilege Escalation. Diese werden in späteren Abschnitten vorgestellt.
Container vs. VM
An dieser Stelle bietet sich ein Blick auf die Unterschiede von VMs und Containern an: VMs sind isolierter als Container, da der Hypervisor der VM vom Host auf Hardware-Ebene abstrahiert und in der VM ein eigenes Betriebssystem (dedizierter Kernel) läuft. Im Gegensatz dazu teilen sich Container den Kernel des Host Betriebssystems. Da auf einer VM häufig nur eine Anwendung betrieben wird, hat hier also jede Anwendung ihren eigenen Kernel.
Daher ist es schwerer den Angriff von einer verwundbaren Anwendung auf weitere Anwendungen, die auf dem gleichen physischen Host laufen, auszuweiten. Bei Containern ist es für Angreifer leichter Schwachstellen in der Container Runtime, dem Kernel oder einem Fehler in der Konfiguration auszunutzen, um aus dem Container auszubrechen. Allerdings ist auch bei einer VM ein Ausbruch nicht ausgeschlossen. Für Beispiele zu Container- und VM-Escapes siehe docker-k8s-high-sec-env.
Angriffsszenario
Mit diesem Wissen lässt sich der Nutzen der beschriebenen Einstellungen beim Blick auf folgendes Angriffsszenario zeigen: Wie beim Betrieb auf einer VM oder einem physischen Host sind bei einer Webanwendung im Container zunächst Ports exponiert. Um von dort in den Container zu gelangen benötigt ein Angreifer die Möglichkeit aus der Entfernung Anweisungen auszuführen (remote code execution). Dies kann durch Schwachstellen in Bausteinen des Systems (Betriebssystem, Server Software, Plattformen wie Java, Bibliotheken) möglich werden. Ein prominentes Beispiel einer “remote code execution vulnerability” ist CVE-2017-5638. Diese Sicherheitslücke befindet sich im Java Web Framework Apache Struts in allen Versionen kleiner 2.3.32 und 2.5.10.1. Diese Schwachstelle wurde beim Equifax Breach ausgenutzt, bei dem 143 Millionen Kundendatensätze entwendet wurden. Mit einem speziellen HTTP-Request kann diese Schwachstelle ausgenutzt werden, um beliebige Befehle auf dem Host ausführen. Da die Ausführung von vielen Befehlen auf diese Art und Weise umständlich ist, wird als nächster Schritt durch den Angreifer oft eine Anwendung wie netcat
auf den Host (in diesem Fall in den Container) heruntergeladen und gestartet. Über diese wird eine Verbindung zu einem entfernten Control Server im Internet aufgebaut (reverse Shell). Über den Control Server kann der Angreifer dann interaktiv Befehle im Container ausführen, wie man es etwa von SSH kennt. Damit kann der Angriff dann ausgeweitet werden: Mit weiteren Tools kann das Netzwerk nach anderen Hosts und offenen Ports durchsucht werden und über den Container auf von außen nicht erreichbare Dienste zugegriffen werden. Beispielsweise sind MongoDB-Instanzen häufig ohne Authentifizierung erreichbar. Über weitere Schwachstellen ist ein Ausbruch aus dem Container denkbar (“container escape”). Ein Beispiel ist CVE-2019-5736 in der low level Container Runtime runc (wird auch von Docker verwendet). Wenn der Container als User root
ausgeführt wird, ist der Angreifer dann auch root
auf dem Host. Mit dieser Berechtigung kann der Angriff noch viel weiter reichen: Es können alle Container auf dem Node übernommen werden, Konfiguration eingesehen und ggf. sogar der ganze Cluster übernommen werden.
Verteidigungsmaßnahmen
Wie ist es möglich sich gegen solche Angriffe zu verteidigen? Der erste Schritt ist natürlich immer möglichst die aktuellsten Versionen der verwendeten Bausteine des Systems zu verwenden. Gegen unbekannte oder noch nicht behobene Sicherheitslücken (zero day attacks) hilft dies jedoch nicht. Hier können weitere Verteidigungsschichten den Schaden oder eine Ausweitung des Angriffs eindämmen.
Das oben beschriebene Szenario wird auf mehreren Ebenen durch die hier empfohlenen Einstellungen verhindert oder zumindest erschwert:
- Ein unprivilegierter User kann nicht einfach Pakete im Container nachinstallieren. Außerdem ist der Angreifer im Falle eines Ausbruchs aus dem Container nicht
root
auf dem Host, wodurch er im Idealfall dort keine Rechte hat. - Ein read-only root Filesystem verhindert auch für den User
root
das Nachinstallieren von Paketen. Noch viel wichtiger ist, das der Angreifer den Code der Anwendung nicht kompromittieren kann. - Das Verhindern von Privilege Escalation sorgt dafür, dass auch im Falle von Schwachstellen ein unprivilegierter User im Container nicht nachträglich zum User
root
wird. Beispiele für solche Schwachstellen liefert docker-security. - Die Ausführung des Containers ohne Capabilities erhöhen die Isolation des Containers und schränken die Optionen des Angreifers ein. Beispielsweise kann mit der Capability “NET_RAW” ein Man-In-The-Middle Angriff auf die Kommunikation aller Container auf einem Host mittels DNS-Spoofing ausgeführt werden.
- Eine weitere Erhöhung der Isolation des Containers und Einschränkung der Optionen des Angreifers kann man mittels das seccomp Profils erreichen. Eine ganze Reihe von Sicherheitslücken im Kernel, die dadurch nicht ausnutzbar sind, sind auf docker-security (siehe oben) gelistet.
- Hier zeigt sich auch der Nutzen von Network Policies: Sie blockieren die Internetverbindung, die ein Angreifer zum Nachladen von weiteren Tools sowie für reverse Shell verwenden könnte und verhindern einen Übergriff auf eventuell aus dem Cluster erreichbare firmeninterne Netze.
Gerade der Blick auf existierende Sicherheitslücken in der Vergangenheit lässt erahnen, dass hier wahrscheinlich weitere, unbekannte Schwachstellen existieren und in Zukunft sicherlich noch weitere entstehen werden. Insofern bietet ein “least Privilege” Ansatz sogar mehr Sicherheit, als man sich zum jeweils aktuellen Zeitpunkt vorstellen kann. Beispielsweise waren zum Zeitpunkt des Bekanntwerdens von CVE-2019-5736 (siehe oben) keine laufenden Container betroffen, die mit einem unprivilegierten User ausgeführt wurden.
Abschließend soll an dieser Stelle zumindest erwähnt werden, dass man viele der oben erwähnten Angriffsvektoren mit Bezug zum User root
auf Seite des Betriebs durch sogenanntes “User Namespace Remapping” abschwächen könnte. Dabei wird die User ID im Container einer anderen User ID außerhalb des Container zugeordnet. Beispielsweise ID 0 (root
) im Container auf 10000 außerhalb des Containers. Damit hat der Container auf dem Host keine erweiterten Rechte. Container Runtimes wie LXC/LXD oder Podman nutzen dies standardmäßig. Bei Docker ist dies jedoch nicht Standardeinstellung und ein Benutzer eines managed Clusters hat darauf nur begrenzt Einfluss. Da sich diese Artikelserie auf die Benutzung des Clusters und nicht den Betrieb des Clusters fokussiert, wird diese Option nicht weiter erörtert.
Weitere Einstellungen im Security Context
Über diese bis hierher empfohlenen, vom Standard abweichenden Sicherheitseinstellungen bietet der Security Context noch weitere, von denen einige kurz erwähnt werden sollen.
Die Option privileged
ist standardmäßig auf false
und sollte auch so bleiben. Privileged hebt quasi die Isolierung des Containers auf. Dadurch würden alle oben gesetzen Einstellungen nutzlos. Dies wurde ursprünglich entwickelt, um Docker in Docker auszuführen. In bestimmten Situationen wie auf einem CI-Server kann dies zwar nützlich sein. Für ein solches Szenario ist dann aber eine dedizierte Maschine oder Cluster empfehlenswert, der getrennt von den produktiven Anwendungen läuft.
Auch die Linux Security Modules SELinux und AppArmor lassen sich in Kubernetes konfigurieren. Bei beiden mischt sich Kubernetes standardmäßig allerdings nicht ein und überlässt dies der Konfiguration der unterliegenden Container Runtime und dadurch in der Verantwortlichkeit des Cluster-Betreibers. Im Falle von Docker ist ähnlich wie bei Seccomp ein AppArmor “default Profile” aktiv. Wenn AppArmor auf dem Node installiert und aktiv ist, greift es automatisch bei Docker. Wer lieber SELinux verwendet (häufig auf RedHat-basierten Linux-Distributionen) kann dessen Verwendung beispielsweise in den Einstellungen des Docker Daemons aktivieren.
Trotzdem bietet der Security Context mit seLinuxOptions
Einstellungen für SELinux an, falls man pro Container spezielle Einstellungen benötigt. Auch ein spezielles AppArmor-Profil kann ähnlich wie bei Seccomp durch Annotationen gesetzt werden. Generell ist es denkbar eigene, restriktivere seccomp, AppArmor oder SELinux Profile zu schreiben (beispielsweise mit Tools wie bane) und mit diesen Einstellungen zu aktivieren. Dies hat eine Steile Lernkurve und ist aufwändig, insofern wird es in diesem Artikel nicht weiter verfolgt.
Zu guter letzt gibt es seit Kubernetes 1.15 Support für Windows-Nodes, bei denen sich im Security Context bestimmte Sicherheitseinstellungen vornehmen lassen. Das Thema ist zum Zeitpunkt der Artikelerstellung noch sehr neu. Wer den Artikel bis hierher aufmerksam gelesen hat, hat sicher festgestellt, dass sich alle Optionen bisher auf Linux bezogen haben. Insofern sprengt das Thema “Container in Windows” den Rahmen dieses Artikels.
k8s Schulungen mit zertifizierten Trainern
Von der Basis Schulung bis hin zur Prüfungsvorbereitung zum Certified Kubernetes Application Developer, bei uns finden Sie Ihre passende Schulung.
Jetzt Termin findenTooling
Wer seinen Cluster interaktiv auf die Einhaltung bestimmter, empfehlenswerter Optionen prüfen möchte, hat mehrere Tools zur Auswahl. Diese Tools bringen ihre eigenen Regeln mit, die deutlich umfangreicher sind, als die in dieser Artikelserie empfohlenen Punkte, deren Fokus auf einem Kompromiss zwischen Aufwand und Sicherheit liegt. Ein Blick lohnt sich also auf jeden Fall. Mehrere Meinungen helfen bei der Entscheidung welche Optionen für den eigenen Anwendungsfall am besten passen. Die Tools können auch an die eigenen Ansprüche angepasst werden. Es ist auch denkbar die Prüfung dieser Einstellungen im CI/CD Prozess zu automatisieren.
Drei bekannte Tools sind kubesec, kubeaudit und kube-bench.
Letzteres automatisiert die Prüfung der Punkte aus der CIS Benchmark für Kubernetes, die sich, wie im ersten Teil erwähnt, auf den Betrieb des Clusters (API-Server, Kubelet, etc.) fokussiert und wenig Empfehlungen für den Betrieb von Anwendungen auf dem Cluster enthält.
kubesec und kubeaudit enthalten unterschiedliche Punkte, die in Teilen im Artikel genannt werden. kubesec ist nahe an den Empfehlungen dieses Artikels, prüft aber standardmäßig zusätzlich auf das Vorhandensein von resource limits
zum Schutz vor Denial Of Service Angriffen. Diese Einstellung kann jedoch die Antwortzeiten der Anwendung auf dem Cluster verschlechtern (Jac18) und außerdem auch durch umliegende Infrastruktur (Reverse Proxy, CDN) realisiert werden. Daher sollte man beispielsweise diese Prüfung nicht einfach ohne nachzudenken übernehmen.
kubeaudit hat generell sehr viele Prüfungen, deren Ergebnisse erschlagend wirken können. Darunter sind beispielsweise auch die oben erwähnte Prüfung auf resource limits
, sowie auf Network Policies oder das Vorhandensein der Annotation für Seccomp und AppArmor. Bei AppArmor haben Container Runtimes typischerweise standardmäßig schon sinnvolle Default Profiles, die aktiv sind, solange der Betreiber des Clusters sie nicht explizit abschaltet. Diese Einstellung muss also nicht bei jedem Pod wiederholt werden.
Einstellungen im ganzen Cluster forcieren
Apropos wiederholen – die gezeigten Einstellungen im Security Context werden pro Pod oder Container angegeben, müssen also pro Anwendung teilweise mehrfach angegeben werden. Diese Einstellung kann auch ohne Wiederholungen Cluster-weit festgelegt werden, mit sogenannten „Pod Security Standards”. Diese sind die Nachfolger der „Pod Security Policies” welche in der Kubernetes Version 1.21 deprecated sind und in der Kubernetes Version 1.25 gänzlich entfernt werden. Die „Pod Security Standards” bieten noch weitere Möglichkeiten zum Schutz von Node und Container Runtime. Diese haben jedoch eine größere Einstiegshürde, da das Erlauben von Ausnahmen deutlich aufwändiger ist. Daher werden als Kompromiss zwischen Aufwand und Sicherheit in diesem Artikel zunächst die Einstellungen mittels Security Context gezeigt. Dieses Vorgehen kann in der Praxis durchaus Sinn machen: In kleineren Teams kann man sich darauf einigen, welche Optionen gesetzt werden und dies dann sukzessive auf alle Anwendungen ausrollen.
Auch ein Test, ob Anwendungen mit den restriktiveren Sicherheitsseinstellungen noch funktionieren, lässt sich mit dem Security Context leicht durchführen. Als sichere Defaults, als Startpunkt in neuen Clustern oder in größeren Organisationen, beziehungsweise einem größeren Personenkreis mit Zugriff auf den Cluster, dem möglicherweise nicht komplett vertraut wird, könnte ein Erzwingen der Einstellungen notwendig sein. Hierfür lohnt sich dann ein Blick auf Pod Security Standards.
Fazit
Dieser Artikel liefert einige Hintergründe die erörtern, wie die im letzten Teil vorgestellte good Practice für den Kubernetes Security Context zustande kommt: Aufgrund der geringeren Isolation von Containern (verglichen mit VMs) bieten sich mehrere Angriffsvektoren, die sich mittels der Einstellungen im Security Context mit wenig Aufwand abschwächen lassen. Es gibt noch weitere Einstellungsmöglichkeiten im Security Context deren Standardwerte aber als “secure by default” angesehen werden können.