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.
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.
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:
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:
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.
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...
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.
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:
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.
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.
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.
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.
Nun sind alle Zutaten beisammen um die Erstellung eines unprivilegierten Container durchzuführen. Der Ablauf könnte und wird so oder so ähnlich aussehen:
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.
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:
lxcuser veth lxcbr0 10
$ grep lxcuser /etc/subuid /etc/subgid /etc/subuid:lxcuser:100000:65536 /etc/subgid:lxcuser:100000:65536
# cgmanager --daemon -m name=systemd # cgm create all wheezy # cgm chown all wheezy 1001 1001
$ cgm movepid all wheezy $$
$ mkdir ~/.config/lxc $ cp /usr/local/etc/lxc/default.conf ~/.config/lxc
lxc.id_map = u 0 100000 65536 lxc.id_map = g 0 100000 65536
$ lxc-create -t download -n wheezy -- -d debian -r wheezy -a amd64
$ lxc-start -n wheezy
$ lxc-attach -n wheezy
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.
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.
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
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:
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.
Das folgt im Wesentlichen diesem Blogeintrag:
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.
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:
Es scheint ein Problem mit systemd-219 zu sein...
LXC besitzt neben lxc-start und lxc-stop natürlich noch eine Menge mehr an Kommandos.
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.
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!
Ü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.
Hierüber können die cgroup-Werte eines Containers abgefragt oder gesetzt werden.
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.
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.
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.
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
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.
Dieser Befehl benötigt wohl keine weiteren Erklärungen und bewirkt das Gegenteil von lxc-create.
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
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.
Das Kommando friert alle Prozesse im Container zeitgleich via cgroup freezer ein bzw. lässt diese wieder anlaufen.
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
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]
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.
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.
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.
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
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.
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.
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.
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.
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:
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
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