Skip to main content

Best Practices zur Container-Isolierung

Artikel von
Headshot of Maryann Agofure

Maryann Agofure

hero container isolation

29. August 2022

0 Min. Lesezeit

Container sind ein standardisiertes Softwarepaketformat, das eine vorhersehbare und reproduzierbare Möglichkeit bietet, Anwendungen auszuführen. Die Container-Isolierung ist einer der wichtigsten Vorteile containerisierter Anwendungen. Durch den Einsatz von Containern können wir unsere Software von ihrer Umgebung isolieren und so die Konsistenz und Zuverlässigkeit in unseren Entwicklungs- und Staging-Umgebungen erhöhen.

Sie kennen wahrscheinlich Docker-Container oder verwenden sie bereits. Docker-Container erreichen die Isolation mithilfe von Linux-Funktionen wie Control Groups (allgemein als cgroups abgekürzt), Filtern des Secure Computing Mode (seccomp) und Kernel-Namespaces. Es gibt auch andere Container, darunter Sandbox-Container wie gVisor und virtualisierte Container wie AWS Firecracker.

Container sind zwar von Grund auf isoliert, dennoch müssen wir Best Practices umsetzen, um unsere Container wirksam zu isolieren und sicher zu halten. Dieser Artikel beleuchtet die Auswirkungen der Container-Isolierung und beschreibt Best Practices für Sicherheit und Methodik bei Linux-, Sandbox- und virtualisierten Containern.

Was ist Container-Isolierung und wie funktioniert sie?

Wie der Name schon sagt, bedeutet Container-Isolierung, die Laufzeitumgebung einer containerisierten Anwendung vom Host-Betriebssystem und anderen Prozessen auf dem Host zu trennen.

Diese Isolation umfasst verschiedene Bereiche: Dateisystem, Netzwerk, Systemaufrufe und Ressourcennutzung, etwa CPU und Arbeitsspeicher.

Wie die Container-Isolierung technisch funktioniert, hängt vom jeweiligen Container ab. Wie wir sehen werden, stehen mehrere Optionen zur Auswahl.

Verschiedene Ansätze zur Container-Isolierung

Wie bereits erwähnt, befasst sich dieser Artikel mit der Container-Isolierung und ihrem Zusammenhang mit drei verschiedenen Containertypen: Linux-Containern wie Docker, Sandbox-Containern wie gVisor und leichtgewichtiger KVM-basierter Virtualisierung wie AWS Firecracker.

Jeder Containertyp geht die Container-Isolierung anders an und isoliert unterschiedliche Systembereiche. Für jeden Containertyp gelten eigene Best Practices zur Isolierung unserer Container.

Sehen wir uns die Best Practices für die Container-Isolierung der einzelnen Containertypen an.

Linux-Container

Linux-Container wie Docker verwenden cgroups, seccomp-Filter und Kernel-Namespaces, um Container zu isolieren. Mit cgroups lassen sich die Ressourcennutzungsgrenzen für eine Gruppe von Prozessen festlegen. So können cgroups beispielsweise die Nutzung verschiedener Ressourcen begrenzen, darunter Festplatten-E/A, Arbeitsspeicher, Netzwerk, CPU-Zeit und sogar einzelne CPUs in einem Mehrkernsystem. Mit seccomp können wir alle Systemaufrufe eines Prozesses filtern. Zusammen mit der Namespacetechnik des Kernels sorgt diese Filterung dafür, dass Funktionen innerhalb eines Containers eine isolierte Ansicht des Systems erhalten.

Dieser Ansatz ist am einfachsten umzusetzen. Das bedeutet allerdings auch, dass Docker sicherstellen muss, dass alle containerisierten Anwendungen beim Erstellen ihre Namespace-Flags korrekt festlegen.

Der Ansatz von Docker ist vergleichsweise einfach und daher leicht anzuwenden. Diese Einfachheit bedeutet jedoch, dass die Isolation nicht so stark ist wie bei Alternativen wie gVisor. Docker muss bestimmte Systemaufrufe über die Namespace-Grenze hinweg zulassen. Dadurch kann eine Anwendung weiterhin auf Informationen des Hosts zugreifen, wenn es im seccomp-Filter ein gültiges Argument für einen bestimmten Aufruf gibt.

Zum Glück gibt es einige Best Practices, mit denen wir die Container-Isolierung unter Docker optimal nutzen können:

  • Verwenden Sie für jeden Container einen dedizierten Benutzer, indem Sie in Ihrer Dockerfile ein bestimmtes Benutzerkonto angeben. So verhindern Sie, dass ein Container auf die Ressourcen eines anderen Containers, etwa Dateien und Verzeichnisse, zugreifen oder diese ändern kann. Verwenden Sie beim Erstellen neuer Docker-Images und runc-Konfigurationen möglichst Benutzer mit den geringsten Berechtigungen.

  • Beschränken Sie die Fähigkeiten der Container mithilfe der bereits erwähnten Linux-Capabilities. Orientieren Sie sich am Prinzip der geringsten Berechtigungen: Ein Container erhält nur die Rechte, die er für seine Aufgaben benötigt. So schränken Sie ein, was eine Anwendung innerhalb eines Containers außerhalb ihrer isolierten Umgebung sehen oder tun kann.

  • Verhindern Sie, dass nicht benötigte Netzwerkgeräte für einzelne Container konfiguriert werden. So schränken Sie ein, was ein Container in der Netzwerkinfrastruktur des Hosts sehen und tun kann.

  • Beschränken Sie mit cgroups die Ressourcen, die jedem Container zur Verfügung stehen, etwa CPU-Anteile, Speicherseiten, Bandbreite für Block-E/A und mehr.

  • Konfigurieren Sie Kernel-Parameter wie die Anzahl der PIDs, die maximale Stack-Größe und die maximale Anzahl von Threads, die ein Container starten kann. Damit verhindern Sie, dass ein Container den PID-Namespace eines anderen übernimmt oder den Kernel des Hosts durch einen Kernel-Panic zum Absturz bringt.

Zusammengenommen verringern diese Maßnahmen die Angriffsfläche unserer Container, indem sie die Anzahl möglicher Angriffspunkte reduzieren. Die beste Sicherheitsmaßnahme ist natürlich, keinen nicht vertrauenswürdigen Code in unseren Containern auszuführen. Dafür müssten wir jedoch genau wissen, was in jedem Container läuft – in vielbeschäftigten Entwicklungs- und DevOps-Teams, die unter Termindruck arbeiten, ist das selten realistisch.

Wenn wir wissen, dass wir einen nicht vertrauenswürdigen Container ausführen müssen, sollten wir als Nächstes eine Container-Runtime mit einem stärkeren Sicherheitsmodell in Betracht ziehen.

Sandbox-Container

Sandbox-Container bieten dieselben Isolationsmechanismen wie herkömmliche Linux-Container und ergänzen sie um zusätzliche Schutzebenen.

gVisor ist ein leistungsstarker Sandbox-Container und implementiert einen benutzerdefinierten Mini-Kernel im Userspace, der zwischen containerisierten Anwendungen und dem Kernel des Hosts liegt. Er fängt alle Systemaufrufe des Containers ab und prüft sie anhand einer Richtlinie, bevor er sie an den Kernel des Hosts weiterleitet. Außerdem implementiert gVisor einen benutzerdefinierten TCP/IP-Stack, um die Interaktion containerisierter Workloads mit dem Netzwerk stärker zu kontrollieren. Schließlich stellt gVisor einen Dateisystem-Proxy zwischen dem Container und dem Dateisystem des Hosts bereit. Dank dieser mehrschichtigen Prüfungen kann gVisor die Angriffsfläche eines Containers verkleinern, indem es klare Container-Grenzen durchsetzt und gleichzeitig mit den meisten Anwendungen kompatibel bleibt. Da gVisor in Go, einer speichersicheren Programmiersprache, geschrieben ist, ist es zudem wesentlich weniger anfällig für Buffer Overflows und andere Exploits als in C geschriebene Anwendungen wie der Linux-Kernel.

Der Ansatz von gVisor hat jedoch auch Nachteile. So kann das Debuggen von Anwendungen innerhalb von gVisor schwierig sein, da die meisten für den Linux-Kernel entwickelten Tools nicht verwendet werden können. Ein weiterer Nachteil: Weil gVisor keinen standardmäßigen Linux-Kernel verwendet, müssen wir möglicherweise Funktionen neu implementieren, um bestimmte Workloads in gVisor zu unterstützen.

gVisor und andere Sandbox-Container sind zwar ein Fortschritt gegenüber herkömmlichen Linux-Containern, doch könnte ein Containermodell mit noch stärkerer Isolation die sinnvollste Best Practice sein.

Leichtgewichtige virtuelle Maschinen

Im Gegensatz zu Linux- und Sandbox-Containern verfolgen leichtgewichtige Container auf Basis virtueller Maschinen (VMs) wie AWS Firecracker einen ganz anderen Ansatz. Sie verwenden einen Hypervisor wie KVM oder qemu, um für jeden Container leichtgewichtige VMs (in der Regel MicroVMs genannt) zu erstellen. Durch diese starke Isolation bleibt die Angriffsfläche innerhalb der Gast-MicroVMs minimal und lässt sich leicht kontrollieren.

Mit dieser Technik wird es für einen Angreifer sehr schwierig, auf privilegierte Informationen des Hosts zuzugreifen. Gleichzeitig gibt es für einen Container dadurch weniger Möglichkeiten, mit dem Host zu interagieren. So können Container in MicroVMs beispielsweise keine Dateien direkt mit dem Host gemeinsam nutzen.

MicroVMs haben eine kleinere Angriffsfläche als Container, die auf herkömmlichen VMs ausgeführt werden, da sie nur eine Teilmenge der Hardwaregeräte unterstützen müssen, die von regulären Linux-Kerneln unterstützt werden.

Insgesamt sind MicroVMs die beste Wahl, wenn wir eine sehr starke Isolation zwischen Containern benötigen – etwa wenn nicht vertrauenswürdiger Code mehrerer Mandanten auf demselben Server ausgeführt wird.

Der richtige Ansatz für die Container-Isolierung

Unabhängig davon, welchen Containertyp wir verwenden: Für die Container-Isolierung gibt es keine Universallösung. Deshalb müssen wir die folgenden Faktoren anhand der spezifischen Anforderungen jedes Projekts abwägen.

  • Leistung. Linux-Container bieten die beste Leistung, da zwischen Container und Host-Betriebssystem die wenigsten Indirektionsebenen liegen. Sandbox-Container sind messbar langsamer, weil zwischen Container und Host-Betriebssystem zusätzliche Vermittlungsebenen liegen. MicroVMs beeinträchtigen die Leistung am stärksten, da sie für jeden Container eine virtualisierte Hardwareschnittstelle bereitstellen müssen.

  • Sicherheit. Je stärker wir die Isolierung von Containern ausbauen, desto sicherer werden sie. Wie wir gesehen haben, sind Sandbox-Container sicherer als Linux-Container und Container auf Basis von MicroVMs sicherer als beide.

  • Komplexität und Entwicklungszeit. Linux-Container sind vergleichsweise einfach und machen den Großteil der in Produktionsumgebungen ausgeführten Container aus. Es gibt zahlreiche hervorragende Tools und Dokumentationen, die die Entwicklungszeit verkürzen und die Komplexität der Bereitstellung containerisierter Anwendungen verringern. Sandbox-Container und MicroVMs sind deutlich weniger verbreitet, weshalb viele gängige Tools sie nicht unterstützen. Weil sie weniger verbreitet sind, werden sie von weniger Tools und Plattformen unterstützt. Das erhöht Entwicklungszeit und Komplexität, da Entwickler- und DevOps-Teams mehr manuell erledigen müssen, statt sich auf Tools verlassen zu können.

Letztendlich müssen wir zwischen Leistung, Sicherheit und Komplexität abwägen. Wenn wir beispielsweise die Leistung priorisieren, müssen wir möglicherweise Abstriche bei der Sicherheit machen. Priorisieren wir hingegen die Sicherheit, müssen wir möglicherweise Leistungseinbußen und eine längere Entwicklungszeit in Kauf nehmen.

Containersicherheit

Als Entwickler und DevOps-Praktiker müssen wir wissen, wie unsere Container von den Hostsystemen isoliert sind, um die Grenzen dieser Isolation zu verstehen. Container ermöglichen zwar schnelle Entwicklungszyklen, doch sind Best Practices unerlässlich, um unsere Container und Anwendungen zu schützen.

Alle Entwicklungs- und DevOps-Teams sollten auf eine mehrschichtige Verteidigungsstrategie setzen. Die Container-Isolierung ist dabei nur ein Teil des Ganzen. Sie können die Sicherheit Ihrer Systeme verbessern, indem Sie Ihren Code mit einem Code-Scanner auf Schwachstellen prüfen und den Inhalt Ihrer Container mit einem Container-Scanner schützen. Schließlich sollte die Container-Isolierung eine Verteidigungslinie sein – nicht Ihre einzige.

Container-Sicherheit mit Fokus auf Entwickler

Snyk findet und behebt automatisch Schwachstellen in Container-Images und Kubernetes-Workloads.