Die Linux-Basics unter der Container-Haube

Einführung: Was Sie über Docker wissen müssen

Jürgen Brunk, Matthias Albert und Nils Magnus

© Shutterstock / Graphic farm

 

Der Hauptverdienst von Docker liegt in seiner standardisierten Hülle: Einheitliche Beschreibungen und Abmessungen vereinfachen das Verladen, Teilen und Ausführen von Anwendungen. Unter der Motorhaube greift Docker auf Bestehendes zurück: Die Basisfunktionen einer Sandbox sowie Namespaces, Cgroups und Chroot sind schon länger Bestandteil des Linux-Kerns. Wer also die richtigen Kommandos bemüht, kann nachvollziehen, wie es im Maschinenraum von Docker aussieht. Das Wissen darüber nützt nicht zuletzt, um sich einen Eindruck von der Wirkungsweise und Sicherheit des Containers zu verschaffen.

Besuch im Docker-Maschinenraum

Viele Vorzüge von Docker schätzen Anwender erst dann richtig, wenn sie selbst Hand angelegt und versucht haben, seine Funktionen nachzubilden. Dieser Artikel zeichnet mit Linux-Bordmitteln nach, was Docker leistet und wie ein Container funktioniert. Dabei geht es nicht darum, ein besseres Framework, sondern vielmehr Verständnis zu schaffen.

Die einfachste Form der Container-Virtualisierung gibt es schon bemerkenswert lange: Das erstmals 1982 eingesetzte Unix-Kommando chroot verändert das Root-Verzeichnis eines Prozesses und seiner Kinder (Kasten: „Chroot-Umgebungen“). Es spricht sich „change root“ aus und verweist auf den gleichnamigen Systemaufruf im Kernel von Unix- und Linux-Systemen. Damit realisiert es eine sehr einfache Sandbox und verhindert, dass Programme auf Dateien außerhalb der neuen Wurzel zugreifen.

Chroot-Umgebungen
Den Systemaufruf chroot() gibt es schon seit 1979. Unix-Pionier Bill Joy brachte ihn 1982 in BSD-Unix ein – aus ganz ähnlichen Gründen, wie er heute noch eingesetzt wird: Er wollte nämlich die Installation und das Build-System testen, ohne sein eigentliches Betriebssystem zu gefährden. Heute kennt fast jedes unixoide Betriebssystem das oft auch als „Jail“ bezeichnete Prinzip. Der Systemaufruf benötigt besondere Privilegien, die mit Root-Rechten einhergehen. Die Kernelfunktion selbst wechselt nicht in das neue Wurzelverzeichnis hinein. Das ist auch mit Grund dafür, dass die Funktion nicht als Sicherheitsfunktion herhalten kann. Der folgende Code demonstriert, von einem User mit Root-Rechten ausgeführt, wie sich aus einer solchen Sandbox entkommen lässt: Er startet nämlich eine neue Shell, die Zugriff auf die äußere Verzeichnisstruktur hat:

#include <unistd.h>
#define DIR "xxx"
int main() {
  int i;
  mkdir(DIR, 0755);
  chroot(DIR);
  for (i = 0; i < 1024; i++) chdir("..");
  chroot(".");
  execl("/bin/sh", "-i", NULL);
}

Der auf der Kommandozeile genutzte gleichnamige Befehl chroot umschifft diese Probleme aber und sorgt dafür, dass der Aufrufer in die Sandbox wechselt. Sollte der Aufruf mit dem dürren Fehler No such file or directory (ENOENT) scheitern, so liegt dies meist an fehlenden dynamischen Bibliotheken. Welche das sind, zeigt ldd für das zu startende Executable an. Für erste Experimente zum Herantasten bieten sich statisch gelinkte Binarys an, etwa die als /bin/static-sh abgelegte Busybox.

Die auch Jails genannten Umgebungen nutzen Entwickler beispielsweise, um Build Chains zu erstellen, in denen sie in einem abgetrennten Bereich neue Software übersetzen, installieren und testen. Administratoren hingegen sperren gerne bedrohte Dienste wie Bind, Apache oder einen FTP-Server in solchen Containern ein. Auf diese Weise halten sie Einbrecher in einem abgeschotteten Bereich gefangen, sollten diese Sicherheitslücken ausnutzen.

DevOps Docker Camp 2017

Das neue DevOps Docker Camp – mit Erkan Yanar

Lernen Sie die Konzepte von Docker und die darauf aufbauende Infrastrukturen umfassend kennen. Bauen Sie Schritt für Schritt eine eigene Infrastruktur für und mit Docker auf!

Umfangreiche Zutatenliste

Um einen Jail anzulegen, muss der Administrator dem eingesperrten Prozess die von ihm benötigten Bibliotheken, Werkzeuge und Frameworks bereitstellen. Wer sich viel Arbeit sparen möchte, erledigt dies mittels debootstrap und erzeugt sich – quasi das Äquivalent zu einem Basis-Image bei Docker – eine minimale, aber voll funktionstüchtige Debian-Installation. Das Kommando muss mit Root-Rechten ausgeführt werden und erwartet als Argumente die Debian-Version und ein Verzeichnis, das später zum neuen Wurzelverzeichnis wird: $ sudo debootstrap wheezy ./wheezy_chroot. Dieser Vorgang lädt innerhalb einiger Minuten rund 250 Megabyte an Softwarepaketen von externen Mirror-Servern herunter und installiert sie im Verzeichnis wheezy_chroot (Abb. 1). Für andere Linux-Distributionen gibt es ähnliche Werkzeuge und Skripte, etwa febootstrap für Fedora oder pacstrap für Arch Linux. Unter Ubuntu steht ebenfalls debootstrap als Paket bei den Bordmitteln zur Verfügung.

Abb. 1: Mittels „debootstrap“ entsteht im Unterverzeichnis „wheezy_chroot“ eine neue Instanz einer Debian-Installation; anschließend wechselt „chroot“ in diese neue Sandbox hinein

Docker nimmt Anwendern diese Fleißaufgabe ab, nennt das Ergebnis Docker-Image und bietet sie sogar in einem eingebauten Repository an. Für die meisten Aufgaben reichen die vielen dort angebotenen Basis-Images bereits aus. Nur wer darüber hinausgehende Anforderungen umsetzen möchte, sollte einmal einen Blick auf die Vielzahl angebotener Skripte werfen, die solche Miniumgebungen zusammenstellen.

Mit dem Kommando chroot betritt der Administrator seine neue Umgebung. Dazu übergibt er dem Kommando zwei Argumente: zuerst das neue Wurzelverzeichnis wheezy_chroot und dann den Pfad zum Programm, welches als erster Prozess laufen soll. Im Beispiel ist das einfach eine Shell: $ sudo chroot ./wheezy_chroot /bin/bash.

Wer sich in seiner Chroot-Umgebung umsieht, bemerkt auf den ersten Blick nicht viel. Die Dateien und Verzeichnisse, die ls anzeigt, ähneln denen in jedem Unix-Wurzelverzeichnis. Auch ein Wechsel in die Unterverzeichnisse mit cd gelingt wie gewohnt. Die Dateien des Hostsystems bleiben jedoch verborgen.

Zu viel Freiheit

Experimentierfreudige verifizieren mit ping die Erreichbarkeit eines bekannten Hosts über das Netzwerk. Weiterhin lässt sich sogar der Hostname des Containers mittels hostname ändern. Hier zeigen sich jedoch die Grenzen des Jails: Die Namensänderung modifiziert nämlich gleichzeitig den Hostnamen der Gastgebermaschine. Schaut sich der Systemverwalter die aktuellen Prozesse an, meldet ps ihm einen Fehler:

wheezy_chroot# ps ax
Error, do this: mount -t proc proc /proc

Eine Kontrolle mit mount ohne Argumente zeigt, dass im Container bislang keine virtuellen Dateisysteme in der Chroot-Umgebung gemountet sind. Dies lässt sich leicht nachholen und erneut kontrollieren:

wheezy_chroot# mount -t proc proc /proc
wheezy_chroot# mount
proc on /proc type proc (rw)

Eine erneute Auflistung der aktuellen Prozesse mittels ps ax zeigt nun alle Prozesse des Hosts an – nicht nur den bislang einzigen im Container. Auch lassen sich alle Prozesse beenden – ebenfalls diejenigen, die nicht aus dem Jail heraus gestartet wurden. Es wäre sogar möglich, den Host neu zu booten. Auch wenn die Chroot-Umgebung ursprünglich nicht als Sicherheitsfeature entworfen wurde, implementiert sie mit ein paar Abstrichen eine Sandbox auf der Ebene des Dateisystems. Sie isoliert aber weder Prozesse, noch Benutzer oder die Netzwerkressourcen voneinander. Weil das Unix-API teilweise über vierzig Jahre in die Vergangenheit zurückreicht, erscheint es bei der Menge von vorhandenem Code nahezu aussichtslos, die vielen System- und Bibliothekaufrufe allesamt anzupassen. Genau das benötigt aber eine Isolation von Betriebsmitteln, wenn sie generisch funktionieren soll.

Geschrieben von
Jürgen Brunk, Matthias Albert und Nils Magnus

Jürgen Brunk, Matthias Albert und Nils Magnus arbeiten als System Engineers bei der inovex GmbH. Sie befassen sich mit der Automatisierbarkeit, Planung und Sicherheit von skalierbaren Rechenzentrumslösungen.

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.