LUG Erding

Unprivilegierte LinuX-Container


Was passiert dabei im Detail?


Dirk Geschke, LUG-Erding


Letzte Änderung: 31.12.2017


Einleitung

LinuX Container stellen eine leichtgewichtige Virtualisierungslösung dar. Es ist keine vollständige Virtualisierung, es wird auch keine Hardware emuliert. Vielmehr bilden die Container eine Isolation von Prozessen in eigenen Namensräumen und können einer Ressourcenkontrolle unterlegt werden.

Unter Linux gibt es schon ähnliche Verfahren, wie zum Beispiel OpenVZ, VServer oder auch User Mode Linux. Allerdings haben diese bestehenden Verfahren ein sehr gravierendes Problem:

Der Kernel muss dafür gesondert gepatcht werden!

Das klingt erst einmal harmlos, es führt jedoch eine Abhängigkeit ein, die zum Beispiel das Einspielen eines neuen Kernels verhindern können. Das kann wegen Sicherheitsproblemen einmal schnell notwendig werden. Wenn dann jedoch auf den Patch von Dritten gewartet werden muss, kann das gravierend sein. Mitunter wird auch ein neuerer Kernel benötigt, der die aktuelle Hardware unterstützt und für diesen könnte es noch keine Patches geben, etc.

LXC hingegen basiert auf Funktionen die im aktuellen Vanilla-Kernel bereitgestellt werden, es wird somit kein separater Patch benötigt.

Vergleichbar sind Linux Container mit Solaris Containern oder auch den Jails in FreeBSD.

Kennzeichnend ist, dass ein gemeinsamer Kernel genutzt wird. Das schränkt ein, was in einem Container laufen kann, insbesondere die Architektur. Ansonsten kann in einem Container jede beliebige Distribution zum Einsatz kommen oder es können auch nur einzelne Softwarepakete laufen, sofern sie keinen speziellen oder geänderten Kernel benötigen.

Container sind sogar schachtelbar, das heißt in einem Container können weitere Container laufen. Allerdings ist derzeit bei einer Tiefe von 32 Verschachtelungen Schluss, mehr geben einige Namensräume nicht her.

Die Vorteile von LXC sind offensichtlich:

Natürlich gibt es auch Nachteile:

Es gibt in einem unprivilegierten Container eigene Benutzer, ein eigener root-Account, eigene Prozesse mit eigenen PIDs. Das wird durch verschiedene Funktionen erreicht:

Zu diesen Funktionen komme ich noch später.

Um dieses Kapitel abzurunden, liste ich hier noch einige Anwendungsmöglichkeiten auf. Einer der für mich wichtigsten Bereiche stellen dabei realitätsnahe Testumgebungen dar. So kann ein Container verwendet werden um:

Ein weiterer Punkt besteht darin, Dienste abzusichern die möglicherweise unsicher sind oder denen von Haus aus nicht vertraut werden kann:

Bei all dem darf aber eines nicht vergessen werden: Auch wenn aus dem Container nicht ausgebrochen werden kann, so ist ein möglicher Angreifer im Container vielleicht bereits hinter einer Firewall und in einer vielleicht vertrauensvollen Umgebung. In aller Regel steht das Netzwerk dem Container-User zur Verfügung. Das sollte unbedingt bedacht werden.

Sicherlich gibt es auch noch andere, interessante Anwendungsgebiete. So können in einem Container durchaus virtuelle Systeme mit KVM installiert und benutzt werden.

Vorbemerkung

Als Basis für die folgenden LXC-Betrachtungen habe ich ein Debian wheezy System verwendet. Das funktioniert nicht mit dem Standard-Paketen von Debian, dazu waren einige Anpassungen notwendig. So habe ich hier sowohl einen aktuelleren Kernel selber kompiliert und installiert, als auch die aktuellen LXC-Quellen übersetzt. Daher passen die Pfade gelegentlich nicht zu denen, die bei den Distributionen verwendet werden. Darauf sollte eventuell geachtet werden. Aber auch die Pakete login, passwd und uidmap musste ich aus jessie zurückportieren um mit unprivilegierten Containern arbeiten zu können.

Mittlerweile habe ich es zum Teil auch unter jessie am Laufen, jedoch mit SystemV als Initsystem auf dem Host.

Es gibt zwei Arten einen unprivilegierten Container zu betreiben, einmal als normaler Benutzer, das ist der Fall den ich hier im Fokus habe. Dazu sind keine root-Rechte notwendig. Natürlich kann ein unprivilegierter Container auch als Benutzer root gestartet werden, diesen Fall nenne ich einen semi-unprivilegierten Container.

Soll systemd als Init-System auf dem Host zum Einsatz kommen, dann ist (derzeit) nur der letzte Fall möglich. Ein Starten eines unprivilegierten Containers als normaler nicht-root-User ist momentan nicht möglich, sofern systemd auf dem Host läuft.

Da aber in der Regel auf dem Host nur ein minimaler Server laufen sollte, der Rest wandert in Container, stellt es derzeit kein größeres Problem dar, wenn hier weiterhin SystemV als Init-System verwendet wird.

Absicherung

Die Absicherung mit den erwähnten Tools wie Seccomp, AppArmor, SELinux und den Capabilities ist für unprivilegierte Container nicht notwendig und oft auch nicht möglich: Hierfür werden in der Regel root-Rechte benötigt, sei es für die Verwendung der Tools oder für die geplante Absicherung. Da der root-User im Container keine root-Rechte auf dem Host hat, ist es unnütz die nicht-existierenden Rechte beschränken zu wollen.

Es gibt drei Arten einen LXC zu betreiben:

  1. als User root, root im Container ist auch außerhalb root
  2. als User root, root im Container ist nicht root außerhalb, das ist der sogenannte semi-unprivilegierte Fall
  3. als normaler, unprivilegierter Nutzer, root im Container kann dann offensichtlich nicht root außerhalb sein. Er hat noch nicht einmal die UID des unprivilegierten Nutzers außerhalb, das heißt er könnte auch nicht auf die Daten von diesem zugreifen, sofern ihm nicht die Rechte dazu explizit eingeräumt wurden.

Da ich mich hier auf den dritten Fall beschränke, können wir die erwähnten Tools außen vor lassen. Sie werden dann relevant um die root-Rechte innerhalb des Containers auf dem Host einzugrenzen. Das ist nicht nur trickreich, es ist auch fehlerträchtig: Sind wirklich alle Möglichkeiten bedacht worden? Von daher sind die unprivilegierten Container klar zu bevorzugen.

Ein paar Worte zu den einzelnen Tools will ich dennoch schreiben:

Seccomp

Das ist die Sandbox-Erweiterung bei Linux. Darüber können die erlaubten System Calls eines Prozesses gesetzt werden. Klassisch waren das nur read(2), write(2), _exit(2) und sigreturn(2).

Mittlerweile kann aber eine Liste von erlaubten System Calls verwendet werden: Die Beschränkung auf die vier obigen System Calls existiert nicht mehr. Welche jedoch notwendig sind, welche unsicher sind und welche unbedingt untersagt werden müssen, ist keine leicht zu beantwortende Frage.

AppArmor

Das hauptsächlich von Novell entwickelte System basiert auf Kernel-Sicherheitserweiterungen um Anwendungen vor noch nicht bekannten Angriffen schützen zu können. Über Profile kann hier definiert werden, welche Anwendungen auf welche Ressourcen zugreifen dürfen. Da das nicht immer so einfach und klar ist, gibt es 3 Modi: Lernmodus, Anwendungsmodus und Auditmodus. Im ersten Fall wird nur protokolliert, im letzten Fall werden Verstöße gegen die definierte Policy protokolliert. Das dient zum Testen der Policy bzw der Protokollierung. Der Anwendungsmodus ist der, welcher wirklich die Programme schützt indem diese eingeschränkt werden.

Problematisch wird es aber, wenn das Programm lediglich umbenannt wird, dann greift die Policy nicht mehr...

SELinux

Diese Erweiterung hat das gleiche Ziel wie AppArmor, verwendet aber einen anderen Ansatz. Es stammt maßgeblich von der NSA. Die Konfiguration ist jedoch recht komplex, so dass die meisten es einfach abschalten anstatt es korrekt zu konfigurieren.

SELinux führt auch Sicherheitskontexte für Benutzer, Rollen und Typen ein. Das macht es nicht gerade übersichtlicher.

Capabilities

Der root-Account auf einem Linux-System darf alles. Das ist in vielen Fällen zuviel und gelegentlich nicht gewünscht. Daher entstand die Idee, dass dieser noch eingeschränkt werden könnte indem ihm ein paar Fähigkeiten wieder entzogen werden.

So könnte ein root seine CAP_SYS_MODULE-Fähigkeit einfach nach dem Booten und Laden der Module verwerfen. Danach ist es für den Nutzer root nicht mehr möglich ein Modul zu laden. Die eigenen Capabilities können in /proc/self/status gefunden werden:

   # grep Cap /proc/self/status
   CapInh: 0000000000000000
   CapPrm: 0000003fffffffff
   CapEff: 0000003fffffffff
   CapBnd: 0000003fffffffff

Für einen normalen Nutzer sehen diese erwartungsgemäß anders aus:

   geschke@voyager:~$ grep Cap /proc/self/status
   CapInh: 0000000000000000
   CapPrm: 0000000000000000
   CapEff: 0000000000000000
   CapBnd: 0000003fffffffff

Dabei handelt es sich um vererbbare (Inheritable), erlaubte (Permitted) und effektive (Effective) Capabilities. Der letzte Wert, die Bounding Capability, gibt an, welche Fähigkeiten bei einem SUID-Programm wirklich genutzt werden können, quasi ein umask für die Capabilities.

Aus diesen doch eher beschränkt nutzbaren Möglichkeiten für Prozesse ergab sich die Idee Dateisystem-Capabilities einzuführen. Hierbei werden einem Programm gezielt weitere Fähigkeiten gegeben um die Rechte zu erweitern ohne sie gleich als SUID Programm laufen zu lassen.

Ein klassisches Beispiel wäre zum Beispiel das ping-Programm. Dieses läuft normalerweise als SUID-root-Programm:

   -rwsr-xr-x 1 root root 36136 Apr 12  2011 /bin/ping

Das ist notwendig, da das Programm direkten Zugriff auf die IP-Pakete (raw-Socket) benötigt und ein normaler Nutzer es auch verwenden können soll. Wird das s-Bit entfernt, so funktioniert ein ping nur noch als Nutzer root:

   # chmod 755 /bin/ping

Als normaler Anwender funktioniert dann das Kommando ping nicht mehr:

   $ ping www.heise.de
   ping: icmp open socket: Operation not permitted

Nun können aber erlaubte und effektive Capabilities für den Zugriff auf den raw-Socket des Netzwerks vergeben werden:

   # setcap cap_net_raw=ep /bin/ping
   # ls -l /bin/ping
   -rwxr-xr-x 1 root root 36136 Apr 12  2011 /bin/ping

Als normaler Nutzer ist das Kommando ping jetzt wieder nutzbar:

   $ ping -c 1 www.heise.de
   PING www.heise.de (193.99.144.85) 56(84) bytes of data.
   64 bytes from www.heise.de (193.99.144.85): icmp_req=1 ttl=245 time=30.3 ms

   --- www.heise.de ping statistics ---
   1 packets transmitted, 1 received, 0% packet loss, time 0ms
   rtt min/avg/max/mdev = 30.329/30.329/30.329/0.000 ms

Jetzt drängt sich automatisch die Frage auf:


Wie kann erkannt werden, dass Capabilities gesetzt sind?

Es ist auf den ersten Blick (ls -l) nicht erkennbar. Dafür gibt es natürlich auch ein Tool:

   $ /sbin/getcap /bin/ping
   /bin/ping = cap_net_raw+ep

Der Vorteil ist offensichtlich, ping hat nun deutlich weniger Rechte in der Ausführung als die vollen root-Rechte die zu dem Zeitpunkt des Aufrufs auf den Nutzer übergehen.

Das schöne an diesem Beispiel ist, dass dieses Kommando lange genug laufen kann um die Capabilities im /proc-Dateisystem zu ermitteln:

   $ grep Cap /proc/20864/status
   CapInh: 0000000000000000
   CapPrm: 0000000000002000
   CapEff: 0000000000002000
   CapBnd: 0000003fffffffff

Jetzt wird es interessant: CAP_NET_RAW ist gesetzt, das bedeutet:

   $ grep CAP_NET_RAW /usr/include/linux/capability.h
   #define CAP_NET_RAW          13

Wie kann nun von den 2000 auf 13 gefolget werden? Ganz einfach, die 2000 ist als Hexadezimaldarstellung zu sehen. Umgerechnet in das Binärformat ist das eine Eins mit 13 Nullen.

Notabene: Die Capabilities fangen bei Null an! Die erste Capability hat nur eine eins, also keine Nullen.

chroot()

Mittels einem chroot() kann ein beliebiges Verzeichnis als neues root-Verzeichnis für einen Prozess definiert werden. Alle weiteren von diesem gestarteten Prozesse, haben jetzt das gleiche root-Verzeichnis.

Primär wird chroot() dafür verwendet um ein Betriebssystem zu installieren, es wird von CD/DVD/USB-Stick gebootet, die Partitionen werden auf dem Zielsystem angelegt, formatiert und gemountet. Dann wird ein minimales System darin installiert (Bootstrapping), ein chroot() dorthin durchgeführt und der Rest in der neuen Umgebung installiert.

Manche verwenden auch ein chroot() um Prozesse zu isolieren, sei es aus Sicherheitsgründen oder um eigene Umgebungen zu schaffen.

Ein Problem besteht jedoch: Aus einer chroot()-Umgebung kann relativ einfach wieder ausgebrochen werden. Dazu müssen zwar root-Rechte in dieser Umgebung vorliegen, das ist jedoch oft erreichbar. Das Entkommen ist kein Fehler von chroot(), es ist vielmehr laut Posix explizit möglich.

Nun wäre das für Container mehr als ungünstig, daher wird hier die Linux-Variante pivot_root() verwendet. Diese ist ähnlich, es kann jedoch nicht mehr entkommen werden. Diese Funktion wird auch von der initrd verwendet. Nach dem Booten wird häufig diese RAM-Disk-Datei verwendet, sie läuft im RAM und mountet die Verzeichnisse. Anschließend wird dessen zentraler Mountpunkt als Ursprung für das Betriebssystem verwendet. Dafür ist der Systemaufruf pivot_root() eingeführt worden.

Namespaces

Seit einiger Zeit bietet der Kernel die Funktion der Namensräume, auch Namespaces genannt. Darüber können Prozesse isoliert werden, sie haben unterschiedliche Sicht auf das Gesamtsystem, insbesondere sehen sie zum Beispiel die Prozesse vom Host nicht mehr. Es gibt unterschiedliche Bereiche, es kommen auch von Zeit zu Zeit neue hinzu.

Erwähnenswert ist auch, dass dafür kein Hypervisor, wie bei klassischen Virtualisierungen, notwendig ist.

Der Kernel stellt dazu zwei neue System Calls bereit:

setns() betritt einen existierenden Namespace, verschiebt den aufrufenden Prozess in diesen Namensraum.
unshare() erzeugt einen Namensraum und setzt den aktuellen Prozess in diesen

Der System Call clone() wurde um entsprechende Flags erweitert. Die Systemfunktion clone() ist ein Analogon zu fork(), kann aber dabei neue Namespaces erzeugen und den Prozess dorthin verschieben.

Jeder Namespace bekommt einen eigenen Inode, über diesen werden sie verknüpft. Um zu sehen, in welchen Namespaces sich der aktuelle Prozess befindet, hilft ein Blick in das Verzeichnis /proc/self/ns. Hier sind die Namespaces als symbolische Links auf Inodes dargestellt:

   $ ls -l /proc/self/ns
   total 0
   lrwxrwxrwx 1 geschke geschke 0 Feb 18 14:24 ipc -> ipc:[4026531839]
   lrwxrwxrwx 1 geschke geschke 0 Feb 18 14:24 mnt -> mnt:[4026531840]
   lrwxrwxrwx 1 geschke geschke 0 Feb 18 14:24 net -> net:[4026531969]
   lrwxrwxrwx 1 geschke geschke 0 Feb 18 14:24 pid -> pid:[4026531836]
   lrwxrwxrwx 1 geschke geschke 0 Feb 18 14:24 user -> user:[4026531837]
   lrwxrwxrwx 1 geschke geschke 0 Feb 18 14:24 uts -> uts:[4026531838]

Damit ist schon ersichtlich, dass es derzeit 6 verschiedene Namespaces gibt. Bevor ich auf diese näher eingehe, werfe ich noch einen Blick auf die Tools, die einem in diesem Zusammenhang zur Verfügung stehen:

iproute Das Programm ip mit der Option netns kann verwendet werden um mit Netzwerk-Namespaces zu arbeiten
util-linux Hier gibt es die Userland-Programme unshare und nsenter, wie es sich vermuten lässt, rufen diese intern unshare() und setns() auf.
lxc lxc-unshare und lxc-usernsexec sind analog zu den von util-linux, aber mitunter aktueller
shadow newuidmap, newgidmap und die Verwendung von subuid, subgid

Anzumerken ist, dass diese Programme nicht immer aktuell sind, so fehlt das Programm nsenter bei Debian wheezy und nicht alle Namespaces werden vom mitgelierferten unshare verwendet. Das liegt auch zum Teil an dem alten Kernel, der von den Distributionen verwendet wird.

Das shadow-Paket teilt sich bei Debian in 2-3 Pakete auf. Bei wheezy sind es passwd und login, das relevante Paket uidmap fehlt. Das ist erst in einer späteren Debian-Version vorhanden, es kann aber leicht zurückportiert werden. Hier sind diese zum Beispiel für wheezy amd64 zu finden:

login http://www.lug-erding.de/deb/login_4.2-1_amd64.deb
passwd http://www.lug-erding.de/deb/passwd_4.2-1_amd64.deb
uidmap http://www.lug-erding.de/deb/uidmap_4.2-1_amd64.deb

Diese Pakete können nur schwerlich aus dem Originalpaket erstellt werden, das shadow-Paket verwendet gleiche Pfade für login und passwd, Debian hingegen einerseits den Prefix / und andererseits /usr. Obige Pakete sind einfach eine Neukompilierung der jessie-Pakete mit der glibc aka libc6 von wheezy.

Ursprünglich waren Namespaces ein Feature von Plan 9, das es leider nie wirklich geschafft hat Realität zu werden, auch wenn die Sourcen schon lange frei verfügbar sind.

Die notwendigen Kernelkonfigurationsparameter lauten:

   CONFIG_NAMESPACES
   CONFIG_UTS_NS
   CONFIG_IPC_NS
   CONFIG_USER_NS
   CONFIG_PID_NS
   CONFIG_NET_NS

Zu finden sind diese unter

   General Setup -> Namespace support 

Die derzeitigen Namespaces sind:

   mnt   Mount-Namespace
   uts   Unix Time Sharing 
   ipc   Inter Process Communication
   net   Netzwerk-Namespace
   pid   Prozess-Namespace
   user  Benutzer-Namepace

Wer sich wundert, warum es kein CONFIG_MNT_NS gibt: mnt war der erste Namespace in Kernel 2.4.19. Dieser wird automatisch aktiv, sobald CONFIG_NAMESPACES gesetzt wird.

CGroups

Ursprünglich wurden die CGroups von Google für Android entwickelt, das war bereits im Jahr 2006. Damals hießen sie noch process container. Ende 2007 wurden sie dann in cgroups, für controlling groups umbenannt.

Während Namespaces die Prozesse, User, etc. separieren, werden hierbei keinerlei Ressourcenbeschränkungen auferlegt. Diese können über cgroups realisiert werden.

Die Liste der vorhandenen cgroups kann in /proc/cgroups eingesehen werden:

   $ cat /proc/cgroups
   #subsys_name    hierarchy       num_cgroups     enabled
   cpuset  0       1       1
   cpu     0       1       1
   cpuacct 0       1       1
   memory  0       1       1
   devices 0       1       1
   freezer 0       1       1
   net_cls 0       1       1
   blkio   0       1       1
   perf_event      0       1       1
   net_prio        0       1       1
   hugetlb 0       1       1
   debug   0       1       1

Normalerweise werden die cgroups nach /sys/fs/cgroup explizit gemountet. Die cgroups pro Prozess sind in der Datei /proc/{pid}/cgroup zu finden, also zum Beispiel für den aktuellen Prozess:

   $ cat /proc/self/cgroup 
   2:name=systemd:/user/2219.user/1661.session

Das ist in Relation zu /sys/fs/cgroup zu sehen:

   $ ls /sys/fs/cgroup/systemd/user/2219.user/1661.session/
   cgroup.clone_children  cgroup.event_control  cgroup.procs  
   notify_on_release  tasks

Dabei ist systemd hier der Ressource-Typ, user ist die User-Klasse.

Und damit kommen wir schon zu den Dateien. Alle cgroups besitzen mindestens vier Dateien:

Die Datei cgroups.procs enthält eine Liste aller Prozesse, die zu dieser cgroup gehören. Wann immer sich etwas an der cgroup ändert, so kann das mittels eventfd() abgefangen werden, sofern diese Datei als FD dafür verwendet wird.

Wird notify_on_release aktiviert (einfach echo 1 > notify_on_release ausführen), so wird beim Verlassen des letzten Prozesses aus der cgroup das bei release_agent hinterlegte Programm aktiv. Dieser release_agent ist am Anfang der cgroup-Kette definiert, also im obigen Fall im Verzeichnis

   /sys/fs/cgroup/systemd/

Einen Prozess zu einer cgroup hinzuzufügen ist recht einfach, es muss lediglich die PID in die Datei tasks geschrieben werden. Jeder Prozess kann dabei nur in einer cgroup pro Ressourcen-Typ vorhanden sein.

Pro Ressourcen-Typ können dabei noch viele weitere Dateien zur Verwaltung von Ressourcen vorkommen. Gemountet werden die cgroups recht einfach:

   # mount -t cgroup none /sys/fs/cgroup/

Dies liefert eine lange Liste an Ressource-Typen:

   # ls /sys/fs/cgroup |wc
   99      99    2173

In jedem dieser Typen können User-Klassen angelegt werden und Prozesse hinzugefügt sowie eingeschränkt, erweitert, etc. werden. Die cgroups können auch, wie oben zu sehen, hierarchisch verwendet werden, das heißt User-Klassen in User-Klassen.

Das ganze ist nun recht verwirrend, es wird schnell unübersichtlich und führt leicht zum Chaos. Daher wurde im Jahr 2013 ein Redesign gestartet. Danach gibt es dann nur noch einen zentralen Prozess der die cgroups verwaltet. Dies ist ab Kernel 3.16 möglich.

Bislang sind aber noch beide Verfahren möglich, also das klassische über mounten von /sys/fs/cgroup und Erstellen von User-Klassen, etc. Sollte dies nicht zum Einsatz kommen, kann ein zentraler Prozess die Verwaltung übernehmen. Im mixed-Fall verwendet der zentrale Prozess alle nicht anders verwendeten Ressourcen.

Das heißt aber auch, wenn ein zentraler Prozess alle cgroups verwaltet, ist die klassische Methode nicht mehr möglich. Alle Änderungen an den cgroups müssen dann über diesen zentralen Prozess erfolgen.

Langfristig wird es zu lediglich einem zentralen Prozess für die Verwaltung kommen.

Alles zusammen genommen

Nun sind alle Zutaten beisammen um die Erstellung eines unprivilegierten Container durchzuführen. Der Ablauf könnte und wird so oder so ähnlich aussehen:

  1. User-Namespace wird zuerst erstellt
  2. in diesem Namespace zum User root werden
  3. , dieser ist außerhalb des Containers unprivilegiert
  4. weitere Namespaces generieren, das ist nun als root im User-Namespace möglich
  5. Netzwerkinterfacepaar veth erstellen
  6. eine Hälfte an den Host verschieben und dort in eine Bridge einhängen
  7. Bind-Mounten relevanter Dateien und Verzeichnisse nach /dev im root-Verzeichnis des Containers
  8. cgroup zur Ressourcenverwaltung einrichten
  9. pivot_root in das root-Verzeichnis des Containers
  10. starten von init im Container

Ob das jetzt die tatsächliche Reihenfolge ist, kann ich im Detail nicht sagen, so könnte die cgroup-Ressourcenverwaltung auch später erfolgen. Aber im Großen und Ganzen sollte es sich so abspielen. Ein Ausnahme hier ist die Erstellung der veth-Devices. Sie werden durch ein SUID-Progamm auf dem Host erstellt und die dann die ein Hälfte in den Container verschoben.

Der init-Prozess übernimmt zum Schluss die Kontrolle und mountet dann auch /proc und /sys, richtet alles weitere ein wie bei einer normalen Linux-Distribution auch.

Erstellen eines unprivilegierten Containers

Vorab gibt es noch ein Problem mit dem Netzwerk. Wir können zwar ein veth-Paar im Netzwerk-Namespace erstellen und in andere verschieben. Aber als unprivilegierter Benutzer können wir dieses nicht an den Host übertragen oder dort erstellen und in einen Container verschieben.

Um das Problem zu lösen, stellt LXC das Programm lxc-user-nic bereit. Dieses läuft auf dem Host mit SUID root. Das ist das einzige Programm, dass ein normaler User mit root-Rechten ausführen kann. Damit kann ein veth-Interfacepaar am Host erstellt werden, eine Hälfte wird dem Container zugeordnet und die andere Hälfte wird auf dem Host einer Bridge hinzugefügt. Diese Bridge muss dafür bereits auf dem Host angelegt worden sein, sie muss also existieren wenn der Container gestartet wird.

Jetzt haben wir alle Einzelteile für die Erstellung eines unprivilegierten Containers beisammen. Der Prozess ist nun einfach und könnte so aussehen:

  1. Erlauben eines Benutzers ein veth-Netzwerkinterface auf dem Host zu erstellen, in eine Bridge hinzuzufügen und die zweite Hälfte dem Container zuordnen. Das geht über Einträge in /usr/local/etc/lxc/lxc-usernet, zum Beispiel:
       lxcuser  veth   lxcbr0  10
    
    Dadurch kann der Benutzer lxcuser bis zu 10 veth-Interfaces an den Host übertragen und in die Bridge lxcbr0 einbringen.
  2. Sicherstellen dass der Benutzer SubUIDs und SubGIDs hat:
       $ grep lxcuser /etc/subuid /etc/subgid 
       /etc/subuid:lxcuser:100000:65536
       /etc/subgid:lxcuser:100000:65536
    
    Bei einem aktuellen shadow-Paket werden diese Einträge durch adduser automatisch erfolgen. Ansonsten können sie von Hand (als root) hier hinzugefügt werden.
  3. Starten von cgmanager, erstellen einer User-Klasse in allen cgroup-Ressourcen, sowie der Übertragung der User-Klasse an den unprivilegierten User:
       # cgmanager --daemon -m name=systemd
       # cgm create all wheezy
       # cgm chown all wheezy 1001 1001
    
    Hier ist 1001 die UID und GID des Nutzers lxcuser und kann über id -u lxcuser bzw. id -g lxcuser ermittelt werden. Der Name wheezy für die User-Klasse ist beliebig, ich wähle hier meist den Namen des Containers.
  4. Verschieben der Shell des unprivilegierten Nutzers in die User-Klasse, auch cgroup genannt:
       $ cgm movepid all wheezy $$
    
    Dabei stellt $$ die PID der aktuellen Shell dar.
  5. Erstellen von Verzeichnissen und Kopieren einer default-Konfiguration
       $ mkdir ~/.config/lxc
       $ cp /usr/local/etc/lxc/default.conf ~/.config/lxc
    
    Anfügen von zwei Zeilen an die Konfigurationsdatei:
       lxc.id_map = u 0 100000 65536  
       lxc.id_map = g 0 100000 65536  
    
    Der Buchstabe gibt an, ob es sich um UID (u) oder GID (g) handelt, die Zahl danach entspricht der ersten UID im Container, die zweite Zahl ist dann die UID außerhalb des Containers. Die letzte Zahl gibt an, wie viele UIDs und GIDs im Container verwendet werden können. Nebenbei: Linux ist nicht an die maximale Zahl von 65536 UIDs gebunden.
  6. Erstellen eines Containers durch ein Download-Template, zum Beispiel:
       $ lxc-create -t download -n wheezy -- -d debian -r wheezy -a amd64
    
    Das lädt das Image direkt vom Server von linuxcontainer.org. Die Liste der verfügbaren minimalen Distributionen ist hier zu finden:

    http://images.linuxcontainers.org//meta/1.0/index-user

    Theoretisch können diese auch selber erstellt werden. Aber durch das UID/GID-Mapping verbunden mit den Besonderheiten des Bind-Mountens von Devices, ist es nicht so trivial wie ein einfaches Bootstrapping im privilegierten Fall.
  7. Starten des Containers:
       $ lxc-start -n wheezy
    
    Das kann noch durch ein --daemon gleich im Hintergrund erfolgen, ansonsten ist die Console im Terminal zu finden. Ab LXC-1.1 hat sich die Eigenschaft geändert, hier wird der Container automatisch im Hintergrund gestartet. Soll dennoch die Console im Terminal verbleiben, ist die Option -F zu verwenden.
  8. Mit dem Kommando
       $ lxc-attach -n wheezy
    
    kann eine Shell im laufenden Container als root gestartet werden. Das kann notwendig sein, wenn beispielsweise keine IP-Adresse per DHCP vergeben wird. Es gibt auch per default kein root-Passwort mehr wie in früheren Templates, das heißt ein Einloggen an der Console ist nicht möglich, solange kein Passwort für root gesetzt wurde. Das kann ebenfalls mit lxc-attach erledigt werden.

    Vorsicht: Bei lxc-attach sind /sbin und /usr/sbin nicht in $PATH, das sollte als erstes vorangestellt werden um Fehler durch nichtgefundene Systemprogramme zu vermeiden.

Das ist im Grunde alles, damit läuft bereits ein Container. Dieser ist in der Regel minimal ausgestattet, es laufen nur eine handvoll Prozesse, wobei die meisten getty-Prozesse sind.

Details

Die Container landen per default hier:

   ~/.local/share/lxc/{Container-Name}/

In diesem Verzeichnis gibt es nach der Installation eine config-Datei für den Container sowie das rootfs-Verzeichnis. Dies ist das root-Verzeichnis des Containers, hierhin wird beim Start des Containers ein pivot_root() durchgeführt.

Dabei wird für die Erstellung der config-Datei, wie oben bereits erwähnt, auf diese Datei zurückgegriffen:

   ~/.config/lxc/default.conf

Die Datei kann von der systemweiten Datei übernommen und erweitert werden:

   cp /usr/local/etc/lxc/default.conf ~/.config/lxc/default.conf

Neben den Anpassungen für das ID-Mapping sollte hier auch noch die Netzwerkkonfiguration stehen, also zum Beispiel:

   lxc.id_map = u 0 100000 65536  
   lxc.id_map = g 0 100000 65536  

   lxc.network.type = veth
   lxc.network.link = lxcbr0
   lxc.network.flags = up
   lxc.network.hwaddr = 00:16:3e:xx:xx:xx

Dies bewirkt, dass ein veth-Paar erstellt wird, eine Hälfte landet als eth0 im Container, die andere Hälfte wird als ein veth{random} auf dem Host angelegt und in die Bridge lxcbr0 integriert. Diese Bridge muss auf dem Host zu diesem Zeitpunkt existieren.

Der Name des veth-Interfaces kann auf dem Host durch den Parameter lxc.network.veth.pair angegeben werden. Das verhindert die Verwendung eines zufälligen Namens. Allerdings wird diese Funktion bei unprivilegierten Containern aus sicherheitsgründen ignoriert.

Bei mehreren Containern ist es daher mitunter schwierig das passende Gegenstück auf dem Host zu finden. Hier hilft dann das Kommando lxc-info weiter. Zusammen mit der Option -n Containername wird eine Liste von Werten des Containers ausgegeben, unter anderem auch das Gegenstück der Netzwerkkarte auf dem Host.

Der Part xx:xx:xx ist korrekt und kein Fehler. Dieser Teil der MAC-Adresse wird beim Anlegen eines Containers durch gültige, zufällige Werte ersetzt.

In der config-Datei können weitere Einstellungen vorgenommen werden, so kann durch die Einträge:

   lxc.cgroup.memory.limit_in_bytes = 1G
   lxc.cgroup.memory.memsw.limit_in_bytes = 2G

der RAM auf 1GB und der SWAP (RAM+SWAP) auf 2GB eingeschränkt werden.

Da der Container im Home-Verzeichnis des Users liegt und mit diesem geteilt wird, empfiehlt es sich hier ein eigenes Volume für den Container zu mounten, also per LVM oder BTRFS. Damit ist das System vom Rest abgeschottet und gleichzeitig im Volumen begrenzt. Das ist in meinen Augen die bessere Lösung als die Verwendung von Dateisystemquotas.

Basis-wheezy-Container

Das Erstellen eines unprivilegierten Containers, ist nun einfach. Wie schon bereits erwähnt, reduziert es sich auf den Download eines Templates direkt aus dem Internet.

Dabei ist das root-Dateisystem, es wird heruntergeladen und entpackt, rund 100 MB groß. Demzufolge hängt die Dauer der Installation von der Internetanbindung und der Geschwindigkeit des Downloadservers ab und kann schon einmal 5-10 Minuten dauern.

In diesem Fall ist ein Debian-Template (-d wie Distribution), das Release ist wheezy (-r wie Release) und es wird eine amd64-Architektur verwendet (-a, wie Architektur).

Mit -n muss der Name des Containers angegeben werden, dieser ist beliebig, ich habe hier wheezy gewählt, da das auch installiert wird. Ich hätte aber auch debian, test oder linuxchen nehmen können...

Als erstes sollte die eigene Shell immer in die passende cgroup verschoben werden. Die hatte ich vorher als Benutzer root angelegt und gab den Namen für alle Ressource-Typen mit wheezy an:

   $ cgm movepid all wheezy $$

Der nächste Schritt ist einfach ein Download, entpacken des root-Dateisystems, etc:

   $ lxc-create -t download -n wheezy -- -d debian -r wheezy -a amd64
   WARN: could not reopen tty: Permission denied
   WARN: could not reopen tty: Permission denied
   WARN: could not reopen tty: Permission denied
   WARN: could not reopen tty: Permission denied
   The cached copy has expired, re-downloading...
   Setting up the GPG keyring
   Downloading the image index
   Downloading the rootfs
   Downloading the metadata
   Unpacking the rootfs
   
   ---
   You just created a Debian container (release=wheezy, arch=amd64, variant=default)
   
   To enable sshd, run: apt-get install openssh-server
   
   For security reason, container images ship without user accounts
   and without a root password.
   
   Use lxc-attach or chroot directly into the rootfs to set a root password
   or create user accounts.

Das war es schon, nun kann der Container gestartet werden. Dabei wird allerdings per default die IP-Adresse per DHCP geholt. Wer keinen DHCP-Server am Laufen hat, sollte entweder vorher die IP-Adresse auf statisch umstellen oder den Timeout von DHCP abwarten.

Ein Problem gibt es noch:

   $ ls -ld .local/share/lxc/wheezy/rootfs/
   drwxr-xr-x 22 100000 100000 4096 Feb 21 23:50 .local/share/lxc/wheezy/rootfs/

Die Dateien gehören nicht dem User lxcuser, der hat, wie oben erkannt werden kann, die UID und GID 1001. Das ist die erste gemappte UID/GID, also root aus dem Container. Mit anderen Worten, der lxcuser kann diese Dateien nicht ändern und umgekehrt.

Soll dennoch darin editiert werden, bleiben zwei Möglichkeiten: Entweder als User root, der darf schließlich alles. Oder der unprivilegierte Anwender begibt sich in den User-Namespace:

   $ lxc-usernsexec -m u:0:100000:65536 -m g:0:100000:65536 
   WARN: could not reopen tty: Permission denied
   # 

und schon entspricht der eigene Account root im Namespace, außerhalb vom Namespace jedoch nicht:

   $ ps aux |grep bin/sh
   100000   29580  0.0  0.0   4196   484 pts/6    S+   13:56   0:00 /bin/sh

Der Benutzer hat jetzt die richtige UID um Dateien im Container editieren zu können. Ich nutze das hier um die IP-Adresse, Netzmaske und Gateway statisch zu setzen:

   # cd .local/share/lxc/wheezy/rootfs/etc/network 
   # vi interfaces
   # cat interfaces
   auto lo
   iface lo inet loopback
   
   auto eth0
   iface eth0 inet static
   address 10.1.0.78
   netmask 255.255.255.0
   gateway 10.1.0.1

Bei der Gelegenheit, sollte auch gleich noch der richtige Nameserver in die Datei resolv.conf eingetragen werden. Das sind alles Einstellungen, die sonst per DHCP verteilt werden.

Hinsichtlich dem Netzwerk wird ein veth-Paar erzeugt, die eine Hälfte gehört dem Host und wird der Bridge lxcbr0 zugeordnet, die andere wird in den Container verschoben.

Auf dem Host habe ich das per /etc/network/interfaces so geregelt:

   auto lxcbr0
   iface lxcbr0 inet static
   bridge_ports none 
   address 10.1.0.1
   netmask 255.255.255.0

Es ist leicht zu erkennen, dass das physikalische Interface des Hosts hier nicht beigefügt ist. Zum Testen ist es auch nicht erforderlich, wenn aber hier eth0 hinzufügt würde, so wäre der Container dann direkt auf dem Kabel angeschlossen.

Da ich aber ohnehin derzeit mit WLAN arbeite, ist es vermutlich nicht möglich: Die meisten WLAN-Chips sind nicht bridgefähig.

Stattdessen kann durchaus mit NAT gearbeitet werden, so dass der Container sich mit dem Netzwerk verbinden kann. Dann ist er zwar nicht direkt erreichbar, es können aber Softwareupdates oder auch neue Pakete per Internet installiert werden, also alle ausgehenden Verbindungen sind so möglich. Eingehend kann es per Destination-NAT ermöglicht werden.

Also sollte hier auf dem Host noch das ausgeführt werden:

   # iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE
   # echo 1 > /proc/sys/net/ipv4/ip_forward

Der letzte Punkt ist wichtig, damit der Host die Pakete überhaupt weiterleitet.

Ist alles eingetragen, so kann der Container einfach gestartet werden:

   $ lxc-start -n wheezy -F

Die Option -F verwende ich hier, weil ich LXC-1.1 installiert habe, da hat sich der default derart geändert, dass der Container automatisch als Daemon gestartet wird.

Das dauert dann in der Regel rund eine Sekunde und der login-Prompt erscheint:

   Debian GNU/Linux 7 wheezy console
   
   wheezy login: 

Herunterfahren geht leicht aus einem anderen Terminal, schließlich können wir uns noch nicht einloggen:

   $ lxc-stop -n wheezy

Dabei werden eine Menge von Fehlermeldungen ausgegeben die daher rühren, dass der Superuser im Container nicht wirklich root auf dem Host ist und daher zum Beispiel den swap gar nicht deaktivieren kann oder die Hardware-Uhr (hwclock) setzen darf. Die sollten aber alle ignoriert werden, ebenso wie die Fehlermeldungen beim Starten. Die können ohnehin bei der Geschwindigkeit nicht gelesen werden.

Sollen sie dennoch gelesen werden, so können die Meldungen mit -L in eine Datei geschrieben werden. Das Ergebnis, bereinigt um Escape-Sequenzen, sieht dann so aus:

   INIT: version 2.88 booting
   Using makefile-style concurrent boot in runlevel S.
   Cleaning up temporary files... /tmp /run /run/lock /run/shm.
   mount: permission denied
   mount: permission denied
   mount: permission denied
   mount: permission denied
   mount: permission denied
   mount: permission denied
   Mount point '/proc/sysrq-trigger' does not exist. Skipping mount. ... (warning).
   Mount point '/dev/console' does not exist. Skipping mount. ... (warning).
   Mount point '/dev/full' does not exist. Skipping mount. ... (warning).
   Mount point '/dev/null' does not exist. Skipping mount. ... (warning).
   Mount point '/dev/random' does not exist. Skipping mount. ... (warning).
   Mount point '/dev/tty' does not exist. Skipping mount. ... (warning).
   Mount point '/dev/urandom' does not exist. Skipping mount. ... (warning).
   Mount point '/dev/zero' does not exist. Skipping mount. ... (warning).
   Mount point '/dev/console' does not exist. Skipping mount. ... (warning).
   Mount point '/dev/tty1' does not exist. Skipping mount. ... (warning).
   Mount point '/dev/tty2' does not exist. Skipping mount. ... (warning).
   Mount point '/dev/tty3' does not exist. Skipping mount. ... (warning).
   Mount point '/dev/tty4' does not exist. Skipping mount. ... (warning).
   Activating lvm and md swap...done.
   Checking file systems...fsck from util-linux 2.20.1
   done.
   Mounting local filesystems...done.
   /etc/init.d/mountall.sh: 59: kill: Illegal number: 8 1
   Activating swapfile swap...done.
   Cleaning up temporary files....
   Setting kernel variables ...done.
   Configuring network interfaces...done.
   Cleaning up temporary files....
   INIT: Entering runlevel: 3
   Using makefile-style concurrent boot in runlevel 3.

   Debian GNU/Linux 7 wheezy console

   wheezy login: 

Wie kann sich nun eingeloggt werden, wenn es, wie die Installationsmeldung besagte, keine Accounts gibt?

Den root-Account gibt es sehr wohl, er hat nur kein Passwort. Das hilft nur bedingt, denn zum Setzen des Passwortes, sollte man eingeloggt sein. Dafür gibt es aber auch ein Tool: lxc-attach. Es kann von einem anderen Terminal aus gestartet werden:

   $ lxc-attach -n wheezy
   root@wheezy:/# 

Jetzt kann das Passwort für root gesetzt werden. Es gilt aber zu beachten, dass per default im Pfad die Verzeichnisse /sbin und /usr/sbin fehlen. Das ist fatal, wenn Software nachinstalliert werden soll. Es bietet sich also an, den Pfad entsprechend zu erweitern:

   root@wheezy:/# export PATH=/sbin:/usr/sbin/:$PATH

Anschließen kann nun das Passwort gesetzt werden:

   root@wheezy:/# passwd

   Enter new UNIX password: 
   Retype new UNIX password: 
   passwd: password updated successfully

Als nächstes werfen wir einen Blick auf die laufenden Prozesse:

   Tasks:   8 total,   1 running,   7 sleeping,   0 stopped,   0 zombie
   %Cpu(s):  3.5 us,  0.9 sy,  0.0 ni, 95.5 id,  0.2 wa,  0.0 hi,  0.0 si,  0.0 st
   KiB Mem:  16163980 total,  3165440 used, 12998540 free,     1040 buffers
   KiB Swap: 31250428 total,        0 used, 31250428 free,  1971948 cached
   
   PID USER      PR  NI  VIRT  RES  SHR S  %CPU %MEM    TIME+  COMMAND
   1 root      20   0 10664 1704 1568 S   0.0  0.0   0:00.01 init  
   1247 root      20   0 14592 1772 1612 S   0.0  0.0   0:00.00 getty
   1248 root      20   0 14592 1744 1596 S   0.0  0.0   0:00.00 getty
   1249 root      20   0 14592 1784 1624 S   0.0  0.0   0:00.00 getty
   1250 root      20   0 14592 1684 1528 S   0.0  0.0   0:00.00 getty
   1251 root      20   0 14592 1720 1560 S   0.0  0.0   0:00.00 getty
   1252 root      20   0 17868 3024 2548 S   0.0  0.0   0:00.02 bash
   1301 root      20   0 21660 2228 1924 R   0.0  0.0   0:00.00 top

Wie leicht zu erkennen ist, läuft fast nichts, es ist auch fast nichts installiert. Die meisten, auch viele gewohnte Programme fehlen. Sie können jederzeit leicht nachinstalliert werden.

Ein ps -x liefert auch keine längere Liste, es läuft wirklich fast nichts in einem Basis-Container:

   root@wheezy:/# ps x
   PID TTY      STAT   TIME COMMAND
   1 ?        Ss     0:00 init [3]  
   1247 console  Ss+    0:00 /sbin/getty 38400 console
   1248 tty1     Ss+    0:00 /sbin/getty 38400 tty1 linux
   1249 tty2     Ss+    0:00 /sbin/getty 38400 tty2 linux
   1250 tty3     Ss+    0:00 /sbin/getty 38400 tty3 linux
   1251 tty4     Ss+    0:00 /sbin/getty 38400 tty4 linux
   1252 ?        S      0:00 /bin/bash
   1318 ?        R+     0:00 ps x

Der init-Prozess läuft, wie erwartet, hier mit der PID 1. Außerhalb des Containers ist das natürlich anders:

   # ps waux | grep init | grep -v root
   100000    4970  0.0  0.0  10664  1704 ?        Ss   14:15   0:00 init [3]  

Offensichtlich wird root auf die UID 100000 außerhalb des Containers gemappt. Hierüber können dann auch leicht die anderen Programme gefunden werden:

   # ps -u 100000
   PID TTY          TIME CMD
   4970 ?        00:00:00 init
   6316 pts/8    00:00:00 getty
   6317 pts/0    00:00:00 getty
   6318 pts/1    00:00:00 getty
   6319 pts/2    00:00:00 getty
   6320 pts/3    00:00:00 getty
   7910 pts/7    00:00:00 bash

Jetzt dürfte wohl klar sein, warum ein Container deutlich weniger Ressourcen benötigt als eine vollständige VM.

Wie angeraten, empfiehlt es sich einen OpenSSH-Server zu installieren, vorher sollte aber noch die Datenbank der verfügbaren Programme aktualisiert werden:

   root@wheezy:/# apt-get update

Anschließend kann weitere Software installiert werden, zum Beispiel:

   root@wheezy:/# apt-get install openssh-server

Spielen mit dem Container: Memory

Soll ein memory-Limit gesetzt werden, so muss die zugehörige cgroup explizit beim Booten aktiviert werden. Diese ist, wie schon erwähnt, aus Performancegründen per default deaktiviert.

Ob es beim Booten aktiviert wurde, kann im dmesg gesehen werden:

   [    0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-3.18.4 root=UUID=9a684dea-2340-48f2-8b66-f597f8b6e9f5 ro cgroup_enable=memory quiet splash "acpi_osi=!Windows 2012"
   [    0.000000] Kernel command line: BOOT_IMAGE=/boot/vmlinuz-3.18.4 root=UUID=9a684dea-2340-48f2-8b66-f597f8b6e9f5 ro cgroup_enable=memory quiet splash "acpi_osi=!Windows 2012"
   [    0.000000] please try 'cgroup_disable=memory' option if you don't want memory cgroups
   [    0.018873] Initializing cgroup subsys memory

oder natürlich in /proc/cmdline, dort muss ein cgroup_enable=memory vorzufinden sein.

Aktiviert wird ein Memory-Limit, wie auch schon erwähnt, in der lokalen Konfigurationsdatei des Containers, also in unserem Fall hier:

   $ grep memory ~/.local/share/lxc/wheezy/config
   lxc.cgroup.memory.limit_in_bytes = 1G
   lxc.cgroup.memory.memsw.limit_in_bytes = 2G

Es drängt sich jetzt eine Frage auf: Funktioniert das auch wirklich?

Erster Versuch: Wir allozieren einfach nur Speicher mit zum Beispiel:

   $ cat  realloc.c
   #include <stdio.h>
   #include <stdlib.h>

   #define SIZE 1024*1024*256
   long long count=0;

   int main()
   {
      char *buf;
      buf=malloc(SIZE);
      if (buf==NULL) {printf("first malloc failed\n"); exit(-1);}
      while (1) { 
        count++;
        printf("allocated: %d MB\n",count*256);
        buf=realloc(buf,count*SIZE);
        if(buf==NULL) {
           printf("failed to allocate %d memory\n",count*SIZE);
           exit(-1);
        }
      }
   }

Wir allozieren also in einer Endlosschleife immer Vielfache von 256 MB. Und siehe da, nach Kompilierung und Ausführung landen wir hier:

   allocated: 67108608 MB
   allocated: 67108864 MB
   failed to allocate 0 memory

Das ist etwas mehr als die 2 GB, die wir erlaubt hatten. Funktioniert es also nicht?

Aber Moment: Das sind so ungefähr 64 TB, also doch etwas mehr als in meinem Notebook installiert sind. Das sind, um genau zu sein, knapp 2^46 Bytes...

Das Geheimnis liegt im overcommit_memory, darüber hatte ich schon einmal berichtet:


Overcommit-Memory

Der Default von overcommit_memory ist gewöhnlich Null:

   $ cat /proc/sys/vm/overcommit_memory 
   0

Es wird also so lange Speicher zugeteilt, wie er nicht wirklich genutzt wird. Das ist oft ein Problem, da beim Allozieren von Speicher durchaus der Return-Wert geprüft wird: Kann kein Speicher erhalten werden, so kann darauf entsprechend reagiert werden.

Ist aber der Speicher zugeteilt und er existiert beim Zugriff nicht, so ist es eine kaum abfangbare Katastrophe. Das könnte nur durch einen Signalhandler für SIGSEGV abgefangen werden. Es ist jedoch alles andere als einfach, darauf geeignet zu reagieren. Daher sollte es, zumindest auf einem Server, besser komplett deaktivieren werden. Bei Notebooks/PCs ist es in aller Regel kein größeres Problem und bei schlecht programmierter Software, also welcher die jede Menge Speicher anfordert aber nicht wirklich genutzt, oft die einzige Wahl zu arbeiten.

Aber zurück zu unserem Problem: Wie testen wir nun die Memorygrenze korrekt? Natürlich können wir einfach overcommit_memory abschalten, aber sollte es nicht auch anders gehen?

Ja: Wir modifizieren einfach obiges Programm ein wenig und schreiben Nullen in den allozierten Speicher, dann muss er schließlich auch vorhanden sein:

   $ cat realloc2.c
   #include <stdio.h>
   #include <stdlib.h>
   #include <string.h>

   #define MB 1024*1024*256
   long long count=1;

   int main()
   {
      char *buf;
      buf=malloc(MB);
      if (buf==NULL) {printf("first malloc failed\n"); exit(-1);}
      memset(buf,0,count*MB);
      while (1) { 
        count++;
        printf("allocated: %d MB\n",count*256);
        buf=realloc(buf,count*MB);
        if(buf==NULL) {
           printf("failed to allocate %d memory\n",count*MB);
           exit(-1);
        }
        memset(buf,0,count*MB);
      }
   }

Beachtet das memset() am Ende!

Starten wir einen neuen Test:

   root@wheezy:~# ./realloc2 
   allocated: 512 MB
   allocated: 768 MB
   allocated: 1024 MB
   allocated: 1280 MB
   allocated: 1536 MB
   allocated: 1792 MB
   allocated: 2048 MB
   Killed

Siehe da, es greift. Es können zwar noch immer mehr als 2 GB alloziert werden, beim anschließenden Schreiben in desen Speicherbereich greift das Speicherlimit. dmesg liefert auch ein entsprechendes Ereignis:

   [109641.293087] Memory cgroup out of memory: Kill process 13867 (realloc2) score 1001 or sacrifice child
   [109641.293089] Killed process 13867 (realloc2) total-vm:2101112kB, anon-rss:1027836kB, file-rss:1052kB

Übrigens liefert ein dmesg im Container die gleiche Ausgabe wie auf dem Host.

Und wer overcommit_memory deaktivieren will, muss dies auf dem Host machen, denn im Container fehlen die Rechte dafür:

   root@wheezy:~# echo 2 > /proc/sys/vm/overcommit_memory 
   -bash: /proc/sys/vm/overcommit_memory: Read-only file system

Notabene: Das Minus-Zeichen vor dem bash ist kein Fehler, es ist Kennzeichen dafür, dass es die Login-Shell ist.

Erwähnenswert wäre noch, dass im Container, egal ob der Speicher per cgroup beschränkt wurde oder nicht, stets der komplette Speicher des Systems und dessen Auslastung zu sehen sind:

   root@wheezy:~# free
                total       used       free     shared    buffers     cached
   Mem:      16163980    1227676   14936304          0          0     298196
   -/+ buffers/cache:     929480   15234500
   Swap:     31250428     270512   30979916

Das gleiche gilt auch für das Dateisystem:

   root@wheezy:~# df .
   Filesystem     1K-blocks     Usenet Available Use% Mounted on
   /dev/sdb10     155253692 78427472  76826220  51% /

Die anderen Host-Partitionen sind aber nicht sichtbar. Das ist auch der Grund, warum ich zu einem eigenen Volume pro Container geraten habe, dann wird der Rest vom Host nicht gesehen, Quotas greifen quasi durch die Volume-Grenze.

Die Ausgabe von uptime ist auch die vom Host, das heißt die Load und die Zahl der eingeloggten User ist auch im Container sichtbar.

Google-Chrome im Container

Das folgt im Wesentlichen diesem Blogeintrag:


LXC 1.0: GUI in containers

Zuerst muss noch einiges an Software installiert werden:

   root@wheezy:~# apt-get install wget vim.tiny ca-certificates sudo pulseaudio

Dann wird noch Google zum Repositorium hinzugefügt:

   root@wheezy:~# cat /etc/apt/sources.list.d/google-chrome.list
   deb http://dl.google.com/linux/chrome/deb/ stable main

Damit es funktioniert, muss der GPG-Key vom Google-Repository aufgenommen werden. Da das per https erfolgt, sollte recht sicher sein, dass es wirklich der Google-Key ist:

   root@wheezy:~# wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add --
   OK

Der Rest ist bekannt, zuerst muss der Katalog der verfügbaren Software aktualisiert werden. Es ist leicht erkennbar, dass nun Google dabei ist:

   root@wheezy:~# apt-get update
   Hit http://security.debian.org wheezy/updates Release.gpg
   Get:1 http://dl.google.com stable Release.gpg [198 B]
   Hit http://security.debian.org wheezy/updates Release         
   Hit http://security.debian.org wheezy/updates/main amd64 Packages    
   Hit http://dl.google.com stable Release
   Hit http://security.debian.org wheezy/updates/contrib amd64 Packages 
   Hit http://http.debian.net wheezy Release.gpg
   Hit http://security.debian.org wheezy/updates/non-free amd64 Packages 
   Hit http://dl.google.com stable/main amd64 Packages
   Hit http://security.debian.org wheezy/updates/contrib Translation-en
   Hit http://security.debian.org wheezy/updates/main Translation-en
   Ign http://dl.google.com stable/main Translation-en
   Hit http://security.debian.org wheezy/updates/non-free Translation-en 
   Hit http://http.debian.net wheezy Release       
   Hit http://http.debian.net wheezy/main amd64 Packages
   Hit http://http.debian.net wheezy/contrib amd64 Packages
   Hit http://http.debian.net wheezy/non-free amd64 Packages
   Hit http://http.debian.net wheezy/contrib Translation-en
   Hit http://http.debian.net wheezy/main Translation-en
   Hit http://http.debian.net wheezy/non-free Translation-en
   Fetched 198 B in 1s (131 B/s)
   Reading package lists... Done

Das Installieren selber ist wieder wie üblich, dabei werden nun aber, zumindest in meinem Fall, insgesamt 185 Pakete installiert:

   root@wheezy:~# apt-get install google-chrome-stable

Auf dem Host in ~/.local/share/lxc/wheezy/config werden noch diese Bind-Mounts hinzugefügt, sie sind für den Zugriff auf X11, der Grafikkarte und dem Sound-Device notwendig:

   lxc.mount.entry = /dev/dri dev/dri none bind,optional,create=dir
   lxc.mount.entry = /dev/snd dev/snd none bind,optional,create=dir
   lxc.mount.entry = /tmp/.X11-unix tmp/.X11-unix none bind,optional,create=dir
   lxc.mount.entry = /dev/video0 dev/video0 none bind,optional,create=file

Dieser Hook dient zur Verwendung von PulseAudio:

   lxc.hook.pre-start = /home/lxcuser/.local/share/lxc/wheezy/setup-pulse.sh

Das Skript sieht dabei so aus:

   $ cat /home/lxcuser/.local/share/lxc/wheezy/setup-pulse.sh
   #!/bin/sh
   PULSE_PATH=$LXC_ROOTFS_PATH/home/lxcuser/.pulse_socket
   
   if [ ! -e "$PULSE_PATH" ] || [ -z "$(lsof -n $PULSE_PATH 2>&1)" ]; then
       pactl load-module module-native-protocol-unix auth-anonymous=1 \
           socket=$PULSE_PATH
   fi

Jetzt muss es noch ausführbar gemacht werden:

   $ chmod a+rx /home/lxcuser/.local/share/lxc/wheezy/setup-pulse.sh

Zugleich müssen noch diese Mapping-Änderungen vorgenommen werden:

   lxc.id_map = u 0 100000 1001
   lxc.id_map = g 0 100000 1001
   lxc.id_map = u 1001 1001 1
   lxc.id_map = g 1001 1001 1
   lxc.id_map = u 1002 101002 64534
   lxc.id_map = g 1002 101002 64534

Dadurch wird der LXC-User mit UID und GID 1001 auf eben den gleichen außerhalb gemappt. Es muss dabei Sorge getragen werden, dass der Benutzer lxcuser im Container die gleiche UID/GID bekommt. Dieser kann mit useradd angelegt werden, also etwas wie:

   root@wheezy# useradd -u 1001 -g 1001 lxcuser

Der Container muss nun angehalten werden:

   lxc-stop -n wheezy 

oder einfach im Container mit:

   root@wheezy# halt

Läuft der Container, so kann direkt darauf zugegriffen und google-chrome gestartet werden, anderenfalls muss der Container erst explizit gestartet werden. Dazu hilft dieses Skript:

   $ cat lxc-chrome 
   #!/bin/sh
   CONTAINER=wheezy
   CMD_LINE="google-chrome --disable-setuid-sandbox $*"

   STARTED=false

   if ! lxc-wait -n $CONTAINER -s RUNNING -t 0; then
       lxc-start -n $CONTAINER -d
       lxc-wait -n $CONTAINER -s RUNNING
       STARTED=true
   fi

   PULSE_SOCKET=/home/lxcuser/.pulse_socket

   lxc-attach --clear-env -n $CONTAINER -- sudo -u lxcuser -i \
       env DISPLAY=$DISPLAY PULSE_SERVER=$PULSE_SOCKET $CMD_LINE

   if [ "$STARTED" = "true" ]; then
       lxc-stop -n $CONTAINER -t 10
   fi

Es muss noch ausführbar gemacht werden und schon kann das Skript zum Einsatz kommen:

   $ chmod a+rx lxc-chrome
   $ ./lxc-chrome

Jetzt läuft google-chrome im Container, Audio und Video werden dabei ganz normal verwendet. (Nicht vergessen: cgm movepid all wheezy $$ vorher ausführen!)

Gestartet wird google-chrome ohne setuid-sandbox, das funktioniert in einem unprivilegierten Container nicht.

Das ist immerhin sicherer als es direkt zu starten. Jedoch birgt auch das ein paar Risiken, so kann PulseAudio auf das Sound-Device zugreifen und vielleicht auch das Mikrofon starten. Via X11 kann ebenfalls eine Menge vom Hauptbildschirm abgefangen werden, es könnte auch ein nicht-sichtbares Fenster erstellt und darin die Tasteneingaben protokolliert werden, etc.

Aber hier geht es auch mehr um das Prinzip.

Alternativ funktioniert auch einfach ein ssh -X lxcuser@10.1.0.38 durchzuführen. Dann brauchen keine Devices durchgereicht und es muss nicht die gleiche UID/GID verwendet werden, etc:

   $ ssh -X 10.1.0.78 -l lxcuser google-chrome --disable-setuid-sandbox

Nach Eingabe des Passwortes startet auch schon gleich google-chrome.

Wie dann jedoch der Sound getunnelt wird, bleibt jedem selber überlassen. Aber PulseAudio ist durchaus netzwerkfähig, das heißt das müsste auch leicht ohne ssh-Tunnel zu realisieren sein.

systemd im Container

Das Problem mit systemd liegt an der Umstellung der cgroups. Da diese nun einen primären Ressourcen-Verwalter benötigen, kann systemd in einem unprivilegierten Container dies nur schwerlich sein. Was hier benötigt wird, ist ein cgroup-Namespace. Den gibt es noch nicht, auch wenn schon erste Patches dafür gesichtet wurden:


CGroup Namespaces

Das ist bislang noch nicht in den Linux-Kernel gewandert.

Die LXC-Entwickler haben hier eine Lösung in Form von LXCFS gefunden, darüber können einige cgroups per Bind-Mount in den Container gemappt werden und systemd ist damit einigermaßen zufrieden.

Wer sich erinnern mag: Die Ressourcenverwaltung per cgroups ist kein primäres Ziel von systemd. Es geht vielmehr darum, dass Prozesse nach dem Starten noch überwacht werden können.

Nun werden hier in aller Regel Daemonen gestartet, sie verabschieden sich von allen anderen Prozessen um völlig unabhängig von einem Parent zu sein. Damit kann systemd aber nicht verfolgen, ob der gestartete Daemon noch läuft oder nicht. Eine PID ist zwar noch da, der Bezug zum Parent ist aber nicht mehr offensichtlich.

Also besteht der Trick darin, den Parent in eine eigene cgroup zu stecken. Alle Prozesse die von diesem Parent ausgehen, bleiben in dieser cgroup, auch wenn sonst alles nach klassischer Art zum Daemon geworden ist, siehe zum Beispiel die Manual-Seite zu daemon(3).

Aber gut, wir fangen mit einem Ubuntu-Container an, der passt zum einen besser zu LXC, zum anderen wurden hier ein paar Anpassungen an systemd vorgenommen. Die Version ist auch neuer als bei Debian.

Zuerst erstellen wir einen Ubuntu-Container mit Vivid und amd64-Architektur:

   $ lxc-create -t download -n ubuntu -- -d ubuntu -r vivid -a amd64

Die Installation dauert bei mir 7 Sekunden, dabei wird das Image aber auch aus dem Cache geholt:

   Using image from local cache

Der nächste Schritt ist einfach: Container starten.

Zuerst eventuell das Netzwerk konfigurieren, das kann aber auch später per lxc-attach -n ubuntu nachgeholt werden, ebenso wie das Setzen des root-Passwortes. Der Nameserver kann einfach in der Datei

   /etc/resolvconf/resolv.conf.d/base

eingetragen werden. Daraus generiert ubuntu die Datei /etc/resolv.conf. Das ist aber nur notwendig, wenn es manuell, also nicht per DHCP erfolgen soll. Starten ist wie gehabt, das -F ist für LXC-1.1 notwendig um das implizite --daemon zu unterdrücken:

   $ lxc-start -F -n ubuntu 

Wenn die Netzwerkadresse statisch gesetzt ist, geht es recht zügig. Es dauert aber gut einen Faktor 2 länger als bei wheezy, also gute 2 Sekunden bis zum Login-Prompt. Dafür geht das Herunterfahren deutlich schneller...

Der nächste Schritt besteht in der Umstellung auf systemd:

   root@ubuntu:~# apt-get update
   root@ubuntu:~# apt-get install systemd libpam-systemd systemd-ui

Das zieht dann nur 134 Pakete nach sich. Das dauert etwas länger und es kann schon etwas geahnt werden.

Anschließend kann der Container erst einmal beendet werden:

   root@ubuntu:~# halt

Würde er jetzt erneut gestartet, so wäre noch immer sysv-init aktiv, das heißt es muss dem System mitgeteilt werden, dass nicht mehr /sbin/init das Init-System des Containers ist. Dafür müssen nur diese Zeilen zur config-Datei hinzugefügt werden:

   lxcuser@voyager:~$ tail -2 .local/share/lxc/ubuntu/config 
   lxc.init_cmd = /bin/systemd
   lxc.include = /usr/local/share/lxcfs/00-lxcfs.conf

Die erste Zeile gibt an, dass wir nun systemd als Init-Funktion haben wollen, die letzte ist für LXC-1.0.x notwendig und fügt lediglich einen mount-Hook hinzu. Darüber werden die für systemd benötigten Bind-Mounts erstellt um auf einige cgroups zugreifen zu können.

Damit das funktioniert, muss es diese Dateien auch geben, sie werden von lxcfs bereit gestellt. Dieser Prozess muss als root auf dem Host gestartet werden, beispielsweise so:

   # lxcfs -s -f -o allow_other /usr/local/var/lib/lxcfs

Dabei ist -s erforderlich um keine Threads zu verwenden. Die verwendete Bibliothek libnih-dbus ist nicht thread-safe. Die Option -f ist die naheliegende, sie verhindert, dass der Prozess sich in den Hintergrund begibt. Zum Testen kann es jedoch nicht schaden, wenn das Programm im Vordergrund läuft. Dabei gibt es dann auch noch die Option -d für das Debugging.

Wichtig ist die Option -o allow_other, sie erlaubt es nicht-root-User auf die cgroups zuzugreifen. Das letzte Verzeichnis ist nur der Mount-Punkt für die cgroups, dieser wird für fuse-Mounts durch lxcfs verwendet:

   $ mount |grep lxcfs
   lxcfs on /usr/local/var/lib/lxcfs type fuse.lxcfs (rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other)

Und schon kann der Container mit systemd als Init-System gestartet werden:

   $ lxc-start -F -n ubuntu

Allerdings gibt es eine Vielzahl von Fehlermeldungen und das Booten dauert obendrein deutlich länger, rund 10 Sekunden bis zum Login. Noch schlimmer wird es beim Einloggen, dann dauert es noch einmal gut 2 Sekunden mit einer Menge an Fehlermeldungen:

   start request repeated too quickly for systemd-journald.service
   Unit systemd-journald-dev-log.socket entered failed state.
   systemd-journald.service failed.

Offenbar mag der journald nicht laufen...

Nun kann man den Dienst bestimmt leicht deaktivieren. Die Frage ist nur, warum er nicht läuft?

Ein

   root@ubuntu:~# systemctl start systemd-journald

liefert eine Menge an Fehlern, relevant scheint der hier zu sein:

   systemd-journald-audit.socket failed to listen on sockets: Operation not permitted

Aber welchen Socket mag er wohl öffnen wollen? Ein -d für Debug gibt es da nicht, wohl aber ein status:

   systemd-journald.service - Journal Service
   Loaded: loaded (/lib/systemd/system/systemd-journald.service; static; vendor preset: enabled)
   Active: failed (Result: start-limit) since Sun 2015-03-01 17:33:36 UTC; 1min 6s ago
   Docs: man:systemd-journald.service(8)
      man:journald.conf(5)
   Process: 2309 ExecStart=/lib/systemd/systemd-journald (code=exited, status=1/FAILURE)
   Main PID: 2309 (code=exited, status=1/FAILURE)
   Status: "Shutting down..."

Das hilft nicht wirklich. Ein strace mit der pid der Shell von außerhalb des Containers, könnte helfen. Aber nicht wirklich, systemd unterbricht die Abarbeitung wenn der Prozess getraced wird:

   Process 7770 suspended

Also gut, dann versuchen wir es doch einfach mit dem Befehl direkt:

   root@ubuntu:~# /lib/systemd/systemd-journald
   Failed to join audit multicast group: Operation not permitted

Sehr informativ, wirklich. Warum braucht ein journald eine Multicast-Gruppe?

Suchen wir einmal mit -d die Debugmöglichkeiten:

   root@ubuntu:~# /lib/systemd/systemd-journald -d
   This program does not take arguments.

Also gut, hängen wir uns von außerhalb wieder an die bash, folgen allen Aufrufen mit -f und finden:

   Process 28068 attached (waiting for parent)
   [pid  7770] setpgid(2339, 2339)         = 0
   [pid  7770] rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
   [pid  7770] rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
   [pid  7770] close(3)                    = 0
   [pid  7770] close(4)                    = 0
   [pid  7770] ioctl(255, TIOCGPGRP, [1815]) = 0
   [pid  7770] rt_sigprocmask(SIG_BLOCK, [CHLD TSTP TTIN TTOU], [CHLD], 8) = 0
   [pid  7770] ioctl(255, TIOCSPGRP, [2339]) = 0
   [pid  7770] rt_sigprocmask(SIG_SETMASK, [CHLD], NULL, 8) = 0
   [pid  7770] rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
   [pid  7770] rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
   [pid  7770] wait4(-1, Process 7770 suspended

Der Prozess ist nun so lange blockiert, wie der strace daran hängt. Das hilft auch nicht wirklich weiter.

Ein strace im Container, sofern er nachinstalliert wird, hilft ein wenig weiter:

   socket(PF_NETLINK, SOCK_RAW|SOCK_CLOEXEC|SOCK_NONBLOCK, 9) = 7
   bind(7, {sa_family=AF_NETLINK, pid=0, groups=00000001}, 12) = -1 EPERM (Operation not permitted)

Offensichtlich schint ein bind() fehlzuschlagen. Wir wissen immerhin, was hier gebunden werden soll. Jedoch bleibt die Frage: Warum um alles in der Welt braucht systemd einen Multicast-Netzwerksocket?

Eine Suche im Internet liefert das hier:


https://bugs.launchpad.net/ubuntu/+source/systemd/+bug/1411112

Es scheint ein Problem mit systemd-219 zu sein...

LXC-Kommandos

LXC besitzt neben lxc-start und lxc-stop natürlich noch eine Menge mehr an Kommandos.

lxc-ls

Das einfachste und naheliegendste ist wohl lxc-ls. Das besitzt aber wenig Magie, es schaut einfach, ob es im LXC-Verzeichnis Unterverzeichnisse mit einem rootfs und einer config-Datei gibt. Wenn dem so ist, handelt es sich offensichtlich um einen Container:

   $ lxc-ls  
   jessie        ubuntu        wheezy        wheezy3       
   jessie1       utopic        wheezy1       wheezy_basic  
   p1            vivid         wheezy2       wheezy_old    

Wie zu erkennen ist, habe ich doch einige Container zum Spielen angelegt. Spannender sind da schon eher die Optionen wie --active, diese zeigen, welche Container gerade laufen:

   $ lxc-ls --active
   wheezy   wheezy1  

Mit --stopped können angehaltene Container angezeigt werden lassen. Das ist aber, wenig überraschend, die Liste aller Container ohne die laufenden...

Etwas schöner ist die fancy-Ausgabe mit --fancy (auch -f):

   $ lxc-ls --fancy
   NAME          STATE    IPV4       IPV6  GROUPS  AUTOSTART  
   ---------------------------------------------------------
   jessie        STOPPED  -          -     -       NO         
   jessie1       STOPPED  -          -     -       NO         
   p1            STOPPED  -          -     -       NO         
   ubuntu        STOPPED  -          -     -       NO         
   utopic        STOPPED  -          -     -       NO         
   vivid         STOPPED  -          -     -       NO         
   wheezy        RUNNING  10.1.0.78  -     -       NO         
   wheezy1       RUNNING  10.1.0.4   -     -       NO         
   wheezy2       STOPPED  -          -     -       NO         
   wheezy3       STOPPED  -          -     -       NO         
   wheezy_basic  STOPPED  -          -     -       NO         
   wheezy_old    STOPPED  -          -     -       NO         

Die Spalte GROUPS kann via lxc.group in der config-Datei gesetzt werden. Darüber kann man dann lxc-ls mit Hilfe der -g Option weiter einschränken.

Mit der Option --nesting können auch verschachtelte Container aufgelistet werden, also Container in Containern.

lxc-attach

Das Programm hatten wir schon, es erlaubt es in einem laufenden Container eine Shell zu starten oder über -- getrennt, ein beliebiges Kommando, zum Beispiel:

   $ lxc-attach -n wheezy -- uptime   
   15:45:48 up  7:32,  0 users,  load average: 0.16, 0.22, 0.23

Dabei fällt einmal mehr auf: Das ist nicht die uptime des Containers, es ist die vom Gesamtsystem, also eigentlich die vom Host. Dafür gibt es offenbar noch keinen Namespace.

Wichtig ist dieser Befehl insbesondere nach der Installation eines unprivilegierten Containers, dann ist nämlich noch kein Passwort für root vergeben und ein Einloggen an der Konsole schlägt folglich fehl.

Zu beachten ist erneut, es kann nicht oft genug erwähnt werden, dass der Nutzer root im Container eine Shell erhält, es ist jedoch nicht der Pfad für den Superuser angepasst, das heißt ein

   export PATH=/sbin:/usr/sbin:/usr/local/sbin:$PATH

wäre nicht so verkehrt. Ansonsten wird es schwierig weitere Software zu installieren, manche notwendigen Programme werden nicht gefunden!

lxc-autostart

Über dieses Kommando können alle Container gestartet werden, die lxc.start.auto in der Konfigurationsdatei gesetzt haben. Dadurch können dann beim Booten die Container automatisch aktiviert werden. Das ergibt jedoch bei unprivilegierten Containern wenig Sinn, diese sollen als normaler User gestartet werden und nicht als root-User beim Booten des Hosts. Hier muss ein anderer Weg gewählt werden.

lxc-cgroup

Hierüber können die cgroup-Werte eines Containers abgefragt oder gesetzt werden.

lxc-checkconfig

Dieses kleine Programm zeigt an, ob der Kernel LXC unterstützt oder in wie weit es Einschränkungen gibt. Allerdings sollte diesem Shellskript nicht zu weit vertraut werden, bei mir liefert es unter anderem:

   Cgroup memory controller: missing

Ein Blick in das Shellskript zeigt:

   echo -n "Cgroup memory controller: "
   if [ $KVER_MAJOR -ge 3 -a $KVER_MINOR -ge 6 ]; then
      is_enabled CONFIG_MEMCG
   else
      is_enabled CONFIG_CGROUP_MEM_RES_CTLR
   fi

Also mindestens Kernel >= 3 und mindestens eine Minorversion >= 6. Mein Kernel ist aber:

   $ uname -a
   Linux voyager 4.0.0-rc6 #1 SMP Sat Apr 4 17:13:41 CEST 2015 x86_64 GNU/Linux

Die Minorversion ist kleiner als 6, dennoch funktioniert der memory cgroup-Controller! Es ist offensichtlich ein Fehler im Skript.

lxc-checkpoint

Das ist ein wichtiger Aspekt von Containern, sie können per criu (Checkpoint Restore In Userspace) eingefroren und wieder restauriert werden. Auf einem System ist das unbedeutend, wenn jedoch das Abbild des Containers auf einem zweiten System vorliegt, kann darüber eine Live-Migration erfolgen. Das Abbild des Containers kann dabei durch ein zentrales, gemeinsames RAID realisiert werden oder via Tools wie glusterfs.

Das wird auch in Verbindung mit LXD verwendet um eine Live-Migration zu realisieren, dabei wird auch der komplette Container kopiert.

Allerdings funktioniert das nicht als normaler Benutzer, hierfür sind doch root-Rechte erforderlich. Damit reduziert sich der Einsatzzweck zumindest auf semi-unprivilegierte Container, also welche die zwar mit subuids/subgids arbeiten, aber als Benutzer root gestartet werden.

Als normaler Benutzer liefert es:

   $ lxc-checkpoint -n wheezy -D /var/tmp/wheey-checkpoint
   lxc_container: lxccontainer.c: criu_ok: 3678 Must be root to checkpoint

   Checkpointing wheezy failed.

Als User root funktioniert es aber auch nur, wenn lxcfs nicht verwendet wird. Dennoch scheitert es hier:

   # lxc-checkpoint -n wheezy -D /var/tmp/cp-wheezy -P ~lxcuser/.local/share/lxc/
   Checkpointing wheezy failed.

Damit es funktioniert, müssen weiterhin folgende Einträge in der config-Datei des Containers vorhanden sein:

   lxc.tty = 0
   lxc.console=none
   lxc.cgroup.devices.deny = c 5:1 rwm 

Diese Einträge verhindern die Konsole und die virtuellen Konsolen. Ab jetzt kann sich dann nur noch per lxc-attach verbunden werden oder über das Netzwerk, sofern es konfiguriert wurde.

lxc-clone

Wie es der Name sagt, lässt sich damit ein bestehender Container einfach kopieren. Dabei gibt es zwei Verfahren: copy und snapshot. Das erste ist einfach, es kopiert das rootfs, das zweite basiert auf den Dateisystemfähigkeiten einen Snapshot zu erstellen, also in aller Regel ist es dann LVM oder BTRFS. Es sind aber auch aufs, overlayfs und zfs als Möglichkeit genannt.

lxc-config

Listet alle konfigurierbaren Punkte auf oder deren Werte, also zum Beispiel:

   $ lxc-config -l
   lxc.default_config
   lxc.lxcpath
   lxc.bdev.lvm.vg
   lxc.bdev.lvm.thin_pool
   lxc.bdev.zfs.root
   lxc.cgroup.use
   lxc.cgroup.pattern

und

   $ lxc-config lxc.lxcpath
   /home/lxcuser/.local/share/lxc

lxc-console

Ab lxc-1.1 startet lxc-start den Container per default im Hintergrund, vorher ging dies mit --daemon. Mit dem Kommando lxc-console kann dennoch an die Konsole gelangt werden. Mit der Option -t kann sogar eine Verbindung mit einer virtuellen Konsole des Containers hergestellt werden. Das Beenden der Konsole erfolgt mit dem Escape-Character der mit -e angegeben werden kann und der Taste q. Per default ist das CTRL-a als Escape-Character und in Kombination mit q wird die Verbindung getrennt.

lxc-destroy

Dieser Befehl benötigt wohl keine weiteren Erklärungen und bewirkt das Gegenteil von lxc-create.

lxc-device

Zum Handhaben von Devices im Container ist dieser Befehl gedacht, aktuell wird nur das Kommando add unterstützt. Darüber lassen sich weitere Geräte zum Container hinzufügen. Das funktioniert aber nur als Benutzer root, also nicht mit unprivilegierten Containern und als normaler Anwender:

   $ lxc-device -n wheezy1 add eth1 
   lxc: lxc_device.c: main: 107 lxc-device must be run as root

lxc-execute

Das ist vermutlich ein Äquivalent zu Docker: Es startet den Container, führt das angegebene Programm aus, wartet auf das Ende und beendet den Container wieder. Dieser darf also vorher nicht laufen, zum Beispiel:

   $ lxc-execute -n wheezy1 -- bin/ls
   init.lxc.static: utils.c: mount_fs: 160 failed to mount /proc : Device or resource busy
   init.lxc.static: utils.c: mount_fs: 160 failed to mount /dev/shm : No such file or directory
   bi   boot  etc   init.lxc.static  lib64  mnt  proc  run   selinux  sys  usr
   bin  dev   home  lib              media  opt  root  sbin  srv      tmp  var

Die Fehlermeldungen sind eine Folge davon, dass ich den Container als einen Unprivilegierten gestartet habe.

lxc-freeze und lxc-unfreeze

Das Kommando friert alle Prozesse im Container zeitgleich via cgroup freezer ein bzw. lässt diese wieder anlaufen.

lxc-info

Der Befehl liefert interessante weitere Angaben über einen laufenden Container. Für nicht-aktive Systeme gibt es lediglich diese Ausgabe:

   $ lxc-info -n wheezy1
   Name:           wheezy1
   State:          STOPPED

Für aktiv laufende Container ist die Ausgabe durchaus gehaltvoll:

   $ lxc-info -n wheezy1
   Name:           wheezy1
   State:          RUNNING
   PID:            8818
   IP:             10.1.0.4
   CPU use:        0.68 seconds
   BlkIO use:      260.00 KiB
   Memory use:     4.57 MiB
   KMem use:       0 bytes
   Link:           vethF5TXD9
   TX bytes:      2.63 KiB
   RX bytes:      508 bytes
   Total bytes:   3.12 KiB

Mittels Kommandozeilenoptionen lassen sich auch nur einzelne Felder ausgeben, zum Beispiel:

   $ lxc-info -n wheezy1 -p
   PID:            8818

oder

   $ lxc-info -n wheezy1 -s
   State:          RUNNING

lxc-monitor

Dieses Kommando überwacht Statusänderungen eines Containers, so liefert die Ausgabe (der Befehl beendet sich nicht), beim Stoppen eines Containers:

   $ lxc-monitor -n wheezy1
   'wheezy1' exited with status [0]
   'wheezy1' changed state to [STOPPING]
   'wheezy1' changed state to [STOPPED]

lxc-snapshot

Das ist ebenfalls unspektakulär, es wird ein Snapshot des laufenden Containers erstellt. Das vertraut aber darauf, dass das zugrunde liegende Dateisystem Snapshots erstellen kann. Es ist also kein wirkliches LXC-Feature. Es funktioniert daher auch nicht als nicht-root-User.

lxc-start und lxc-stop

Das benötigt keiner weiteren Erklärung, es startet bzw. beendet einen Container. Ab der Version 1.1 muss die beim Starten die Option -F verwendet werden um zu verhindern, dass er im Hintergrund gestartet wird.

lxc-start-ephemeral

Dies startet eine flüchtige Kopie eines existierenden Containers, das heißt nach Beendigung wird die Kopie wieder gelöscht. Aber auch das funktioniert nicht als normaler Nutzer.

lxc-top

Das ist ein top-Kommando für laufende Container. Ich denke diese Beispielausgabe verdeutlicht die Funktion am einfachsten:

   Container               CPU      CPU      CPU      BlkIO        Mem
   Name                   Used      Sys     User      Total       Used
   wheezy                 1.85     1.65     0.62  200.00 KB    2.27 MB
   wheezy1                0.60     0.66     0.27  260.00 KB    4.57 MB
   TOTAL 2 of 2           2.45     2.31     0.89  460.00 KB    6.84 MB

lxc-unshare

Die mitgelieferte unshare-Funktion einer Distribution zeigt oft nicht alle Möglichkeiten des verwendeten Linux-Kernels. Daher ist diese LXC-Erweiterung eine Alternative, die ich auch schon verwendet hatte.

lxc-usernsexec

Damit kann ein Job in einem neuen Usernamespace gestartet werden, dabei wird dieser Prozess dann als root Benutzer ausgeführt. Es kann hier auch mit -m ein Usermapping verwendet werden.

lxc-wait

Hiermit kann auf ein spezielles Ereignis eines Containers gewartet werden. So wartet

   lxc-wait -n wheezy -s RUNNING

bis der Container läuft. Das kann für weitere Steuerungsaufgaben, zum Beispiel dem Start von Diensten in Verbindung mit diesem Container, genutzt werden.

Zu all diesen Befehlen gibt es natürlich auch Handbuchseiten (man-pages), die nähere Informationen und weitere Optionen beinhalten.

LXD

Bei LXD wird von einem Hypervisor für Container gesprochen. Dabei gibt es drei Komponenten, zum einen ist es der Daemon lxd für die systemweite Konfiguration sowie einem Kommandozeilentool lxc für die Verwaltung der verteilten Container. Die dritte Komponente ist ein Plugin für OpenStack Nova, auf dieses werde ich hier aber nicht näher eingehen.

Das größte Problem ist die Installation von LXD unter Debian wheezy. Während die meisten anderen Projekte rund um LXC entweder auf C oder Python setzen, setzt LXD auf Go. Allerdings wird dafür eine neuere Version benötigt, als bei Debian wheezy verfügbar ist. Daher muss erst eine aktuelle Go-Version, mindestens die Version 1.2, sowie diverse zusätzliche Tools, installiert werden. Erst dann besteht die Möglichkeit LXD zu kompilieren.

Das Ergebnis liefert dann zwei Binärdateien: lxd und lxc. Diese sind dann unabhängig von Go und können ohne weiteres auf anderen Systemen der gleichen Architektur zum Einsatz kommen.

Einer der Vorteile von LXD besteht in einer Live-Migration. Derzeit scheint es hier aber noch ein paar Probleme zu geben, momentan läuft es nur mit privilegierten und semi-privilegierten Containern. Um criu, ein OpenVT-Tool für die Erstellung von Checkpoints und einem Restaurieren von Prozessen, verwenden zu können, muss der Container als Benutzer root gestartet worden sein.

In diesem Fall muss also eine Beschränkung auf den semi-unprivilegierten Container erfolgen. Das heißt er wird als Benutzer root gestartet, dennoch werden die Benutzer im Container auf andere UIDs/GIDs außerhalb via subuid und subgid gemappt. Damit ist der Benutzer root im Container nicht mehr Herr über alles auf dem Host, er kann also auch keine eigenen Devices anlegen.

Dabei stellt lxd eine REST API bereit, die auch über das Netzwerk erreicht werden kann. Die HTTP-ähnliche Kommunikation wird durch TLS abgesichert. Die Authentisierung erfolgt über Server- und Clientzertifikate. Im ersten Schritt wird das Clientzertifikat mit einer Passwortauthentisierung verteilt.

Das ganze LXD-Projekt ist noch in Bewegung, die Dokumentation ist auch mehr als bescheiden. Entsprechend steht auch dieser Kommentar auf der Projektwebseite:

LXD is currently in very active development and isn't yet ready for production use.

Vom Prinzip her muss ein lxd-Dienst auf allen Systemen laufen, auf denen auch Container aktiv oder aktivierbar sein sollen. Dazu muss auch der User root via /etc/subuid und /etc/subgid in der Lage sein, UIDs/GIDs zu mappen. Idealerweise sollte das auf allen Systemen gleich sein, sollte eine Migrationsmöglichkeit verwendet werden.

Die Konfigurationsdateien der Container liegen jetzt auch nicht mehr im Dateisystem neben dem rootfs, sie befinden sich nun in einer Datenbank und werden Profile genannt. Dazu komme ich später.

Da ich alle Sourcen selber kompiliert habe, landen alle relevanten Tools unter /usr/local. Aber irgendwo ist momentan noch ein Pfad hartkodiert enthalten, daher muss erst einmal ein symbolischer Link erzeugt werden:

   # ln -s /usr/local/share/lxc /usr/share

Der erste Punkt, der mir noch ein wenig unklar ist, betrifft die CGroups. Es wird in der Dokumentation nicht ausdrücklich erwähnt, aber ohne CGroups funktionieren die Container nicht. Nur ist unklar, was die korrekte Vorgehensweise ist: Soll der cgmanager verwendet werden oder einfach nur, da der Container als root gestartet wird ist es möglich, /sys/fs/cgroup gemountet werden?

Vermutlich ist es derzeit letzreres. Dazu muss in /etc/fstab dieser Eintrag vorliegen:

   cgroup  /sys/fs/cgroup  cgroup  defaults  0   0

Danach kann ein mount erfolgen:

   # mount /sys/fs/cgroup

Ein Wechsel zwischen dem direkten Mounten und der Verwendung von cgmanager ist in aller Regel mit einem Reboot verbunden, ansonsten gibt es seltsame Fehlermeldungen.

Um Benutzern einer Gruppe Zugriff auf lxd geben zu können, gibt es die Option --group, ferner kann mit --tcp eine Adresse und Port angegeben werden, auf dem lxd dann auf Netzwerkverbindungen wartet:

   # lxd --group lxcuser --tcp 192.168.1.233:8443

Die Container werden aus Images erstellt, dafür werden aber erst eben solche benötigt. Diese können aus dem Internet heruntergeladen und installiert werden. Passend dazu gibt es in den LXD-Sourcen ein Skript:

   # lxd-images import lxc debian wheezy amd64 --alias debian
   Downloading the GPG key for https://images.linuxcontainers.org
   Downloading the image list for https://images.linuxcontainers.org
   Validating the GPG signature of /tmp/tmpnx2z20/index.json.asc
   Downloading the image: https://images.linuxcontainers.org/images/debian/wheezy/amd64/default/20150402_22:42/lxd.tar.xz
   Validating the GPG signature of /tmp/tmpnx2z20/debian-wheezy-amd64-default-20150402_22:42.tar.xz.asc
   Image imported as: 8dd188d795d60252aeb358126c81a8549ba1418743a741de2a3a3e4f32727165
   Setup alias: debian

   # lxd-images import lxc ubuntu trusty amd64 --alias ubuntu
   Downloading the GPG key for https://images.linuxcontainers.org
   Downloading the image list for https://images.linuxcontainers.org
   Validating the GPG signature of /tmp/tmpyn61pu/index.json.asc
   Downloading the image: https://images.linuxcontainers.org/images/ubuntu/trusty/amd64/default/20150403_03:49/lxd.tar.xz
   Validating the GPG signature of /tmp/tmpyn61pu/ubuntu-trusty-amd64-default-20150403_03:49.tar.xz.asc
   Image imported as: d70a5653750aea0835cc0b97a778f518719bf631e73352c8f6dc43f871e067cd
   Setup alias: ubuntu

Jetzt kann ein Benutzer aus der lxcuser-Gruppe einfach einen Container erstellen und starten:

   $ lxc launch ubuntu ubuntix
   Generating a client certificate. This may take a minute...
   Creating container...done
   Starting container...done

Zu beachten ist, dass die Container nach

   /var/lib/lxd/lxc/

installiert wird. Dort sollte auch entsprechend Platz vorgehalten werden.

Das Client-Zertifikat wird nur bei der ersten Verwendung von lxc erstellt und wird im Server installiert. Das wird insbesondere dann relevant, wenn es Zugriffe über das Netzwerk geben soll. Allerdings kann da nicht so einfach per Gruppenrechte zugegriffen werden, das erfordert dann doch etwas mehr Aufwand.

Die Zertifikate werden, zusammen mit der Konfiguration der Container, in einer Datenbank abgelegt:

   /var/lib/lxd/lxd.db

Der einfachste Weg, dieses über das Netzwerk zu realisieren, besteht in der Verwendung einer Passwort-Authentisierung. Ich habe hier zwei Testsysteme, otto (192.168.1.233) und karl (192.168.1.234):

   root@otto:~# lxc config set password geheim
   Generating a client certificate. This may take a minute...

Und auf karl:

   lxcuser@karl:~$ lxc remote add otto https://192.168.1.233:8443/
   Generating a client certificate. This may take a minute...
   Certificate fingerprint: 82 52 61 82 d0 f2 4a 90 70 35 6c de b2 9b a8 04 53 e6 c1 71 e0 a2 a8 e7 7a 2f b2 54 65 6e e1 17
   ok (y/n)? y
   Admin password for otto:
   Client certificate stored at server:  otto

Das ist sicherlich nicht der beste und auch nicht gerade der sicherste Weg. Wenn das Passwort jedoch komplex genug ist, sollte es vertretbar sein.

Welche Server nun verfügbar sind, kann leicht mit list erfragt werden:

   lxcuser@karl:~$ lxc remote list
   local <unix:/var/lib/lxd/unix.socket>
   otto <https://192.168.1.233:8443>

Für local-Zugriffe muss nichts weiter beachtet werden, dass ist der Default-Wert. Für remote-Zugriffe muss dann lediglich der Name mit einem Doppelpunkt verwendet werden:

   lxcuser@karl:~$ lxc list otto:
   +---------+---------+---------------+------+
   |  NAME   |  STATE  |     IPV4      | IPV6 |
   +---------+---------+---------------+------+
   | wheezy1 | STOPPED |               |      |
   | wheezy2 | RUNNING | 192.168.1.111 |      |
   | ubuntix | RUNNING | 192.168.1.112 |      |
   +---------+---------+---------------+------+

Starten und stoppen läuft analog.

Es kann auch per remote Befehle im Container ausgeführt werden und sogar eine Shell erhalten:

   lxcuser@karl:~$ lxc exec otto:ubuntix bash
   root@ubuntix:~# 

Spannend wird eine Migration über das move-Kommando: Es funktioniert derzeit nicht mit unprivilegierten Containern, auch wenn diese durch den User root gestartet wurden. Was jedoch schon funktioniert, ist das Einfrieren der Konfiguration und das Übertragen auf das andere System. Dort kann der Zustand aber nicht restauriert werden, das heißt der Container kann nur noch neu gestartet werden wobei natürlich alle bestehenden Verbindungen abbrechen.

Momentan funktioniert eine Migration nur mit einem privilegierten Container. Dann ist das Kommando recht einfach:

   lxcuser@karl:~$ lxc move otto:ubuntix local:ubuntix

Was notwendig ist, damit ein Container privilegiert gestartet wird und was noch an Anpassungen notwendig ist, wird gleich bei den Profilen noch erläutert.

Ein Tipp, wer es auch testen will: Mit der Environment-Variable TEMP kann ein anderes temporäres Verzeichnis für lxd angegeben werden als /tmp. Die Migrationslogdatei wird in diesem Verzeichnis erstellt und dann am Ende nach

   /var/log/lxd/{container}/migration-{date}.log

umbenannt. Wenn die aber nicht auf der gleichen Partition liegen, /tmp ist oft ein tmpfs, dann funktioniert der rename()-Aufruf nicht und die Datei wird einfach gelöscht.

Es sollten auch die Pakete rsync und netcat-openbsd installiert sein, nicht jedoch netcat-traditional. Letzteres enthält zwar auch ein nc-Kommando, das kennt jedoch die von LXD benötigte Option -U nicht.

Mit dem lxc-Programm kann im Wesentlichen alles erledigt werden, was auch ohne LXD möglich ist.

Profile

Interessant ist hier noch die Frage der Konfiguration der Container. Diese erfolgt über Profile:

   $ lxc config profile list
   default

Das Profil kann editiert werden, es ist auch möglich es zu kopieren und mit einer Kopie zu arbeiten:

   $ lxc config profile copy default myprofile
   $ lxc config profile edit myprofile

Das startet einen Editor, hier kann zum Beispiel das Folgende eingegeben werden, es ist derzeit für das Funktionieren des move-Kommandos notwendig:

      name: myprofile
   config:
     raw.lxc: |-
       lxc.tty=0
       lxc.console=none
       lxc.cgroup.devices.deny=c 5:1 rwm
   devices:
     eth0:
       nictype: bridged
       parent: lxcbr0
       type: nic

Das meiste dürfte selbsterklärend sein.

Nun muss dieses Profil einem Container noch zugewiesen werden, das ist auch über das Netzwerk möglich:

   $ lxc config profile apply otto:ubuntix myprofile
   Profile myprofile applied to ubuntix

Um zu sehen, welches Profil bei einem Container verwendet wird, kann folgendes Kommando verwendet werden, hier für ubuntix:

   $ lxc config show otto:ubuntix
   Profiles: myprofile
   volatile.eth0.hwaddr = 00:16:3e:96:2a:ce
   raw.lxc = lxc.tty=0
   lxc.console=none
   lxc.cgroup.devices.deny=c 5:1 rwm

Um nun einen Container priveligiert starten zu lassen, gibt es auch ein config-Kommando für lxc:

   lxc config set otto:ubuntix security.privileged true

Wenn dies gesetzt ist, so ist der root-User im Container auch der root-User außerhalb des Containers! Das ist sicherlich nicht erstrebenswert, da nun mit anderen Methoden die Sicherheit des Hosts gewährleistet werden muss, wie zum Beispiel mit SELinux, Seccomp, etc. Aber es ist momentan der einzige Weg um eine Live-Migration von einem Host auf einen anderen unterbrechungsfrei zu realisieren.

Das größte Problem besteht darin, dass der Restore der Zustände im User-Namespace erfolgen muss. Der root-User in einem unprivilegierten System hat aber keine Zugriffsrechte um die notwendigen Daten im Kernel zu integrieren. Deswegen ist ein Checkpointing erfolgreich, das kann der root-User von außerhalb des Containers durchführen, der Restore scheitert jedoch daran.

Einige Details dazu sind hier nachzulesen:


User namespace

Update

Ab Kernelversion 4.6 gibt es Namespaces für CGroups. Damit ist es nun problemlos möglich auch systemd-basierte Linuxdistributionen im Container laufen zu lassen.

Ferner ist es nun auch möglich ohne den cgmanager auszukommen. Dazu ist das Modul pam_cgfs.so von LXCFS benötigt. Dieses muss dann über die zwei Dateien

   /etc/pam.d/common-session  
   /etc/pam.d/common-session-noninteractive

über die Zeile

   session        optional        pam_cgfs.so -c freezer,memory,name=systemd

eingebunden werden.

Damit das Ganze nun auch funktioniert, müssen die cgroups vorher manuell gemountet werden. Das übernimmt zum Beispiel das Skript cgroup-mount aus dem Ubuntu-Paket cgroup-lite. Die Quellen dazu sind zum Beispiel hier zu finden: cgroup-lite_1.11.tar.xz.

Fazit

LXC ist zweifellos ein interessantes Projekt. Hat sich nicht jeder schon einmal ein kleines Testsystem gewünscht, das schnell aufgesetzt ist, wenig Ressourcen benötigt und dennoch die Anforderungen an ein Linux erfüllt?

Abgesehen von kleinen Unschönheiten, wie zum Beispiel dass sämtlicher RAM des Hosts gesehen wird, sowie die Load und auch das lokale Filesystem, funktioniert es doch sehr zuverlässig. Diese Ungereimtheiten werden auch erst dann wirklich relevant, wenn jemand in das VServer-Geschäft einsteigen möchte. Dann wäre es von Vorteil auch das zu isolieren.

Und wer weiß, vielleicht kommen entsprechende Namespaces bald?

Im Gegensatz zu OpenVZ baut LXC auf einen vanilla Kernel auf, das heißt wenn es einen neuen Kernel gibt, funktioniert es auch mit diesem. Das ist zum Beispiel bei OpenVZ durchaus ein Problem, das Projekt setzt noch aktuell auf Kernel 2.6.32. Das wird spannend, wenn alle größeren Distributionen auf systemd setzen werden.

Letzteres wiederum bereitet momentan LXC ein paar Probleme, jedoch scheinen die mit LXCFS behebbar zu sein. Wie ich gezeigt habe, kann damit durchaus ein Container mit systemd als Init-System gestartet werden. Der Container benötigt jedoch erheblich länger beim Starten, was bei just diesen eher von Nachteil ist.

Der neueste Entwicklungsschritt mit LXD zeigt in eine interessante Richtung, wer träumt nicht davon, einmal Container ohne größeren Aufwand von einem System auf ein anderes verschieben zu können? Zumindest für produktive Systeme, die so wenig Ausfallzeit wie möglich haben sollen, klingt das interessant. Es können dann die Ressourcen effektiver genutzt oder gar neue Hardware problemlos eingebunden werden. Aber das ist im Moment noch alles in Bewegung.

Eines der vielen Probleme von LXC, auch wenn Version 1.0 noch bis 2019 supportet wird, ist, dass noch viel Bewegung in der Entwicklung ist, was auch das vergangene Jahr gezeigt hat. Gleichzeitig ist die Dokumentation zwar besser geworden als vorher, da gab es praktisch keine. Nun gibt es welche, sie ist aber oft sehr rudimentär. Bei LXD ist der Dokumentationsstand ungleich schlimmer.

Das macht es auch so schwierig herauszufinden, was denn wirklich alles machbar ist und funktioniert. Die vielen Howtos und sonstigen Artikel zu LXC sind oft veraltet und spiegeln die aktuellen Fähigkeiten nicht wieder oder behaupten gar, sie würden nicht funktionieren.

Die offizielle Dokumentation ist spärlich und selbst diese Blogpost-Serie ist langsam out-of-date:

LXC 1.0 Blog Post Series

Ein weiteres Problem sind die Distributionen, die haben selten die neueste Software paketiert. Hinzu kommt, das diverse Kernelfunktionen benötigt werden, die oft nur in den neuesten Kernelversionen vorliegen oder auf Optionen setzen, die der Distributionskernel nicht aktiviert hat.

Ich hoffe, dass ich mit hiermit den Punkt Dokumentation ein wenig vorwärts gebracht habe. Insbesondere im deutschsprachigen Raum gibt es da nicht so viel, zumindest soweit ich es gesehen habe. Aber auch ich kann nicht immer damit aktuell bleiben, die Entwicklung schreitet voran.

Viel wichtiger war mir aber auch der Blick unter die Haube um zu verstehen wie es funktioniert. Erst damit kann ein Vertrauen zu Containern aufgebaut werden: Es entwickelt sich damit ein Gefühl dafür, was technisch zum Einsatz kommt, wie die Dinge realisiert werden.

Dabei liegt für mich einer der wichtigsten Punkte in der Verwendung von Namespaces, auf diese habe ich auch viel Zeit verwendet. Das sollte jedem sofort einleuchten, dass die Sicherheit erhöht ist, wenn der ganze Container schon nicht als Benutzer root gestartet wird.

Noch wichtiger ist die saubere Separation der UIDs/GIDs, so dass diese im Container nicht identisch sind mit welchen auf dem Host. Das ist das Besondere an der Version 1.0 von LXC: Unprivilegierte Container sind damit erst möglich.

Eines sollte jedoch nicht vergessen werden: Die Sicherheit im Container! Denn wer einen Container gekapert hat und darin root ist, kann auch da schon viel Unheil anrichten. Bei Testsystemen hingegen ist das ein weniger gewichtiger Aspekt.


Dirk Geschke, dirk@lug-erding.de