Broken by design: cloud-init
Schimpfen über Linux und Open Source.
Ich hatte in den letzten Jahren gelegentlich erwähnt, dass meine Begeisterung vom Open-Source-/Linux-Bereich deutlich abgekühlt ist, weil man merkt, dass die Software oft immer schlechter wird. Gründe hat das mehrere:
- Die Leute können weniger als früher.
- Es werden im Zuge von Toleranz, Diversität, Frauenförderung usw. immer mehr völlig unqualifizierte Leute in die Projekte reintoleriert.
- Man darf auch nichts mehr sagen, weil jeder, der Kritik äußert, gleich rausfliegt, weil er gegen irgendeinen Code of Conduct verstößt, wenn er sagt, dass irgendwas Murks ist.
- Es ist auch so ein Zeitgeist-Problem, dass die Leute überhaupt nicht mehr darüber nachdenken, ob das andere gefährdert oder sie irgendwelche Fallen aufstellen, weil da so eine trotzige „Mir gefällt es“ oder „Du musst es ja nicht verwenden“–Mentalität herrscht.
Die Open-Source-Szene war mal richtig gut, besonders im Zeitraum zwischen etwa 1995 und 2010, aber etwa so seit 2010..2015 habe ich das Gefühl, dass das rapide bergab geht und da immer mehr Mist produziert wird.
Nun hatten Leser gebeten, ob ich da nicht mal ein Beispiel bringen kann. Jetzt habe ich eines.
Sorry, wenn das jetzt etwas technisch wird, das wird nicht mehr jeder verstehen, aber das muss jetzt halt mal sein, wenn ich das belegen will und soll, worüber ich immer wieder schimpfe.
Automatische Konfiguration
Zur Einleitung: Das ist Stand der Technik, dass man Linux-Systeme nicht mehr nur von Hand konfiguriert. Gerade die Server-Systeme – und die weit überwiegende Mehrzahl von Servern im Internet und in der Cloud laufen auf Linux – werden nicht mehr einzeln von Hand konfiguriert, sondern automatisiert, damit das schnell, in Massen und vor allem schreibfehlerfrei funktioniert. Das ist ganz wichtig, denn das ist genau der Punkt, warum manche dann, wenn sie angegriffen und ihnen die Server zusammengeschossen werden, nach ein, zwei Tagen wieder da sind, und warum andere dann Monate lahmgelegt sind: Weil es von der Frage abhängt, ob man ein System hat, das auf ein frisches Linux vollautomatisch Konfiguration, Software usw. draufspielt, oder ob man da einen hinsetzt, der mit irgendwelchen Dokumentationsfetzen versucht, das tote System von Hand nachzufrickeln.
Nun gibt es da verschiedene Ebene. Es gibt die Systeme für große, komplexe Dinge, die ganze Serverlandschaften aufbauen können, wie Puppet oder Chef.
Dann gibt es so die in der Mitte, die nicht so große Sachen können (oder sehr langsam wären) dafür aber schneller aufzusetzen sind, keinen Server und Client brauchen, wie etwa Ansible.
Und dann gibt es Systeme so im unteren Level, die nur vergleichsweise wenig können, aber dazu dienen, die Maschine überhaupt erst mal in Betrieb zu bekommen und die deshalb nicht auf externe Daten zugreifen können, sondern alles mitbringen müssen, weil zu dem Zeitpunkt das Netzwerk noch nicht funktioniert oder erst einmal die Platte partitioniert werden muss und ähnliches. Das ist zum Beispiel cloud-init, das auf mehreren Distributionen läuft, die Autoinstallation von Ubuntu oder die Konfiguration von Fedora CoreOS.
Dabei ist wichtig zu verstehen, dass die nicht etwa in Konkurrenz zueinander stehen, sondern sich prima ergänzen. Die Low-Level-Systeme wie cloud-init oder CoreOS sind ganz wichtig für virtuelle Maschinen und Cloud-Computing, weil man nur so Maschinen automatisch, schnell, billig und in großen Zahlen konfigurieren kann, beispielsweise Hostnamen setzen, ssh-Zugangsschlüssel setzen, damit man überhaupt mal reinkommt, Routing, NTP und sowas. cloud-init ist dann zum Beispiel in der Lage, automatisch den Puppet-Client zu installieren, zu konfigurieren und zu starten, damit also der unterste Teil der Konfiguration von cloud-init erledigt wird, bis das System überhaupt am Netz ist und läuft, damit dann Puppet kommen kann und dann Webserver, Datenbanken und sowas darauf installiert. Auch Ansible und Puppet können sich gut ergänzen.
Das ist eigentlich so das Prinzip, wie man das hinbekommt, dass Maschinen schnell – und vor allem: kontrolliert, dokumentiert, vorhersagbar, testfähig – und beliebig oft gestartet und konfiguriert werden können, und man da nicht tagelang die Maus schubst, umd das irgendwie hinzudengeln versucht, sondern man seine Server in wenigen Minuten wieder hinbekommt. Es gibt Firmen, die Geld sparen, weil sie ihre Arbeitsumgebung in der Cloud laufen haben und jeden Abend nach Feierabend alle ihre Server löschen, und sie jeden Morgen per automatischer Installation neu erzeugen, weil sie dann für die Nacht und die Wochenenden nicht bezahlen müssen.
Ich habe einiges mit diesen Systemen gemacht. Alle meine eigenen Linux-Rechner (und das sind nicht wenige) und einen Verein habe ich mit Puppet konfiguriert, auch im Job viel mit Puppet gemacht.
Der Webserver, auf dem dieses Blog läuft, ist in der Grundinstallation vom Provider mit cloud-init vorkonfiguriert, dass er gerade so läuft, und die Software habe ich mit Ansible installiert, um es auch mal zu üben. Ich bin aber mit Ansible nicht sonderlich glücklich, weil schon so ein Blog für Ansible recht viel ist und das dann schon nervig langsam und komplex wird. Ich werde das demnächst mit Puppet neu machen. Puppet hat zwar auch so ein paar nervende Macken, aber ist recht leistungsfähig. Zu Chef kann ich nicht viel sagen, damit habe ich noch nichts gemacht.
Aber, ach.
cloud-init
cloud-init ist ein distributionsübergreifendes System, das vor allem für Server, nicht für Desktops gedacht ist. Dokumentation hier. Die Dokumentation ist üppig, aber irgendwie unübersichtlich und vor allem lückenhaft. Die sieht zwar schön volständig aus, aber es gibt immer wieder Dinge, die da nicht drin stehen.
Zum Beispiel die Server-Version von Ubuntu, meistverwendetes Cloud-Server-Betriebssystem, verwendet cloud-init. Auch dann, wenn man lokal einen vom USB-Stick installiert. Oder über deren MAAS-System (Metal as a Service, Verwaltung von Rechner-Farmen.) Man kann auch bei manchen Cloud-Providern beim Starten einer Maschine in der Cloud für Ubuntu-Maschinen ein cloud-init-Skript mitgeben. Und da steht dann beispielsweise drin, dass ein Account für Hadmut Danisch angelegt und da die ssh-Schlüssel eingefüllt werden, die ich brauche, die Zeitzone, deutsche Zeichensatz- und Formateinstellungen und so weiter und so fort, und beispielsweise eben auch, dass man da den Puppet-Client installiert (oder sogar startet), damit man möglichst wenig manuell tun muss und das Ding gleich starten kann.
Schön.
Es ist schon einige Jahre und Ubuntu-Versionen her, da hatte ich damit schon richtig Ärger. Da kannte ich das noch nicht (näher).
Ich hatte ein Maschine installiert, die als physischer Computer vor mir auf dem Tisch stand und einen kleinen Server abliefern sollte. Also per USB-Stick Ubuntu-Server installiert. Dabei kommen Eingabemasken, in denen man den Rechnernamen, Benutzernamen und so weiter angibt. Ich hatte zunächst einen provisorischen Rechnernamen angegeben, nennen wir das jetzt hier mal Banane. Das Ding also fertig installiert, alles OK, dann aber entschieden, dass das Ding einen anderen hostnamen haben müsste, etwa Erdbeere. Das ist normalerweise leicht, man schreibt den neuen Namen in /etc/hostname, passt ggf. /etc/hosts noch an, und könnte den zwar auch im laufenden Betrieb ändern, macht aber besser einen Reboot. Alles andere, die vielen Konfigurationen, in denen der Hostname auch geändert werden muss, werden, wenn man es richtig gemacht hat, von Puppet automatisch angepasst.
Das Ding neu gebootet, und Banane ist wieder da.
Ich so: Moment, das habe ich doch eben auf Erdbeere geändert. Nochmal geändert. Das Ding hält sich jetzt für Erdbeere.
Nach dem nächsten Boot: Wieder Banane.
Bin ich blöd oder die Kiste? Woher weiß die, dass sie Banane heißt/hieß, nachdem ich das gändert habe?
Erster Verdacht: Netzwerk. Der DHCP/DNS-Server gibt den alten Namen weiter. Also Netzwerkkabel raus. Und /etc durchsucht, damit da nirgends mehr Banane steht. Du mögest Erdbeere heißen. Reboot, isoliert, und wieder Banane.
Ei, verflixt, woher hat das vedammte Ding diesen Zeichenstring “Banane”, wenn ich den überall gelöscht habe und keine Außenverbindung besteht?
Nachgeguckt, gesucht, und tatsächlich in den Tiefen des /var-Verzeichnis eine Datei gefunden, in einem cloud-init-Verzeichnis, ganz tief versteckt, in der “Banane” stand, und als ich die gelöscht habe, war der Spuk vorbei.
Grund, mich näher mit cloud-init zu beschäftigen.
Denn bisher, ich hatte mich nicht näher damit beschäftigt, nur etwas darüber gelesen, war ich der Auffassung, dass cloud-init dazu dient, die Maschine beim ersten Booten zu initialisieren und fertig. Was ich nicht wusste, und was damals auch nicht oder nur sehr versteckt und verklausuliert, indirekt stand, dass das System eben nicht nur bei der Installation, sondern bei jedem Booten funktioniert. Der Gedanke dahinter ist, dass man ja auch Maschinen kopieren können soll und dann nicht zwei identische Maschinen laufen, sondern vom Provider neue Hostnamen, IP-Adressen und sowas abgeholt und konfiguriert werden können. Und deshalb beließ es das System nicht dabei, den Namen zu belassen, den ich in /etc/hostname eingetragen hatte, sondern den zu setzen, den es für diese Maschine für richtig hielt. Und es hatte gelernt, dass diese Maschine für alle Zeit und unauflösbar Banane heißen möge. Erst durch Kopie und Laufenlassen auf anderer Hardware wäre es zu einem neuen Hostnamen gekommen.
Und das halte ich für mördergefährlich.
Nicht nur wegen falscher Hostnamen. Sondern auch, weil über cloud-init auch die ssh-Schlüssel und mitunter auch root-Passworte gesetzt werden. Wenn beispielsweise ein Schlüssel oder Passwort kompromittiert ist, würde man den auf dem Rechner ändern, damit man damit nicht einbrechen kann. Was aber, wenn man den ändert, und cloud-init undokumentiert – wie bei meinem Hostnamen – das Zeug wieder zurücksetzt, und damit auch ein möglichwerise kompromittiertes Passwort oder einen kompromittierten Schlüssel wieder in Kraft setzt?
Ich fand das schön mördergefährlich.
Beim Lesen der Doku fand ich dann, dass das System für verschieden Plattformen und Cloud-Provider eine ganze Reihe von Methoden hat, siehe hier, woher die Daten kommen. Denn die werden nur bei der Installation mit einem USB-Stick als Datei fest kopiert, aber in Cloud-Umgebungen dynamisch vom Cloud-Provider geholt und mitunter sogar dynamisch aktualisiert, damit der Provider damit etwa die default-Route, den Nameserver und ähnliches automatisiert wechseln kann.
Und das ist wieder höllengefährlich, weil der Provider damit auch automatisiert einbrechen kann, etwa indem er einfach andere ssh-Schlüssel einträgt. Das wirkte schon wie ein System auf mich, mit dem man vollautomatisch Maschinen aufmachen kann.
Ich hatte mir deshalb angewöhnt, in Umgebungen, die ich voll unter Kontrolle habe und in denen ich solche Aktualisierungen nicht brauche, cloud-init nach der Erstinstallation – für die es sehr nützlich ist – rauszuwerfen. Böse daran ist, dass Ubuntu früher auch nicht sagte, dass da tief drinnen so ein Ding wütet.
Nun wollte ich für einige Tests und Updates eine Reihe von Testmaschinen in der Containerumgebung LXD erzeugen. Ach, dachte ich mir, LXD hat auch cloud-init eingebaut (genauer gesagt, den Übergabemechanismus von Daten an das System), das wäre bequem, weil ich viele Tests machen will und dann den Kram nicht jedesmal von Hand einstellen muss.
Bisschen gemacht, bisschen getan, bisschen gelesen, funktioniert.
Und dann gab es ein Problem.
Eigentlich nämlich sieht cloud-init nicht nur eine Datenquelle für Konfigurationsdaten vorsieht, sondern zwei. Nämlich die user-data und die vendor-data. Die funktionieren im Prinzip gleich, gleiche Syntax und so, haben aber einen unterschiedlichen Zweck. Normalerweise ist es üblich, dass einem ein Hoster eine Maschine konfiguriert bis – wie man so schön sagt – „Oberkante Betriebssytem“ hinstellt. Da also Routen, NTP-Server, Nameserver, mount-Einträge und so ein Kram schon eingetragen sind, und der Nutzer dann dafür verantwortlich ist, die Anwendungssoftware zu installieren. Das hat sich so bewährt.
Und deshalb gibt es da zwei Datenquellen, user und vendor, für die jeweiligen Daten. In die Vendor-Data trägt man eben Routen, DNS, search-domains und so’n Basiskram ein, und in die user-data schreibt der Benutzer, dass er einen Webserver installieren oder eben Puppet starten oder seine Freundin als Benutzer eintragen will.
Das ist schön, dachte ich mir, das ist genau das, was ich gerade brauche. In die user-data schreibe ich, was die Testmaschinen machen sollen, und in die vendor-data schreibe ich, was hier mit meinem Netzwerk zusammenhängt, beispielsweise wo mein Debian-/Ubuntu-Cache läuft oder wie der Puppetserver heißt.
Aber, ach.
Das hat mich heute einige Zeit gekostet herauszufinden, dass das nicht so läuft, wie ich mir das vorgestellt und verstanden habe. Denn die user-data wurden ausgeführt und installiert, die vendor-data aber nicht. Obwohl die Konfigurationsdaten korrekt eingetragen waren.
Seltsamer Effekt: Lasse ich aus Testgründen die user-data weg, werden die vendor-data normal installiert. Sind die user-data aber da, werden nur die installiert.
Ein vernünfiger Grund steht nicht in der Doku.
Man möge in deren #IRC-Channel nachfragen. 90er-Jahre-Feeling.
Also gut, gefragt.
Ja, heißt es, da hätte ich die Doku nicht richtig gelesen und werden auf die Stelle verwiesen, an der es auch nicht steht.
Die User-data überdecken die vendor-data, damit der Benutzer fein auswählen könne, was er vom vendor übernehmen wolle und was nicht.
Die Crux daran ist, dass das meistens verwendete Datenformat YAML ist, und das dann in Form von Hashes und Arrays eingetragen wird. Und der Eintrag des Benutzers überschreibt den des Vendors komplett.
Beispiel:
User: runcmd: - irgendein befehl - touch /punkt1 Vendor: runcmd: - anderer befehl - touch /punkt2
Die Befehle des Vendors werden dann gar nicht mehr ausgeführt, weil der Nutzer seine Befehle unter dem Konfigurationsschlüsselwort “runcmd” angeben muss, die als Array unter dem Schlüssel “runcmd” im großen Konfigurationshash abgelegt sind und damit die Befehle des Providers einfach alle überschreiben, die werden dann nicht mehr ausgeführt.
Dasselbe mit allem anderen. Erzeugt etwa der Benutzer eine Datei, was unter dem Eintrag “write_files” konfiguriert wird:
write_files: - path: /abc content: xyz - path: /def content: XYZ
usw. Erzeugt aber der Benutzer auch nur eine einzige Datei, selbst wenn sie damit einfach gar nicht zu tun hat, muss er das ebenfalls unter “write_files” eintragen und überschreibt damit sämtliche Befehle des Vendors.
Und das kann dann nicht nur dazu führen, dass irgendwas nicht mehr funktioniert, und einen in den Wahnsinn treiben kann, weil die Maschine nicht mehr hochkommt, nur weil man irgendwo eine unwichtige Datei erzeugt, weil man nicht merkt, dass dann etwas anderes fehlt, sondern sowas kann auch zu ernsthaften Sicherheitsproblemen führen, wenn etwa Firewalldateien, /etc/hosts.deny oder so etwas unbemerkt nicht geschrieben werden.
Völlig unpredictable. Da fallen Funktionen komplett aus, nur weil der Benutzer unwissentlich einen ähnlichen Konfigurationstyp verwendet wie Dateien erzeugen oder Befehle ausführen, auch wenn es ganz andere Dateien oder Befehle sind.
Deren Reaktion: Ja, aber es steht doch (verklausuliert und interpretierungsbedürftig) irgendwo in der Doku. So nach dem Motto, dass ein Mist kein Mist sei, wenn in der Doku auch Mist steht.
Und sie sehen es dann nicht ein.
“Works as intended”.
Wenn es aber, sagte ich, works as intended, und das Ergebnis gefährlicher Schrott ist, dann war eben die Absicht schon Murks. Das ist dann eben broken by design, und kein versehentlicher Fehler. Nicht jeder Mist ensteht versehentlich. So mancher Mist ist in vollem Vorsatz gewollt, und dadurch trotzdem kein Stück besser.
Und das merkt man dann eben auch, wenn einem Entwickler versuchen, Workarounds zu erklären, wie man mit dem von ihnen selbst gebauten System noch klarkommen könnte. Wenn einer gleichzeitig sagt, dass ein System so ist, wie es ist, weil er es genau so haben wollte, und dann die workarounds und schrägen Tricks aufzählen muss, die man braucht, um es zum Funktionieren zu bringen, dann ist richtig was faul.
Sie meinten halt, dass der Benutzer damit die Möglichkeit habe, alle Einstellungen des Vendors, also des Dienstleisters, zu überschreiben, wenn sie ihm nicht passen. Aha. Und woher soll er die überhaupt wissen? Und was ist, wenn sie ihm passen und er sie nicht ändern, aber halt irgendwas ganz anderes einstellen will?
Man reagiert pampig:
<minimal> hadmut: as blackboxsw indicated it is working as intended so it is not broken
Kein System könne kaputt sein, wenn sich ein Blödel findet, der es so haben will. Im Zweifelsfall der, der es so entworfen hat.
Dass das aber nicht nur hochgefährlich ist und auch untragbar, weil die Ergebnisse unvorhersehbar sind, das sehen die nicht ein.
Auto ohne Bremsen? Kein Problem, es gibt Leute, die das lustig finden.
Beachtlicherweise hat dann die anscheinend einzige im Chat anwesende Frau (oder jemand mit Frauenname) bestätigt, was ich sage:
<meena> if you think about it for a while tho, you will realize that it makes no sense: vendor-data is often opaque to users, so there could be destructive interactions
Ja, meint sie, wenn man mal für eine Weile drüber nachdenkt (was man anscheinend bisher nicht getan hat), merkt man, dass das Verfahren keinen Sinn ergibt: Der Benutzer weiß oft gar nicht, was der Vendor da einstellt, oder verfolgt das auch nicht ständig, und auf einmal gibt es destruktive Kollisionen, obwohl sich die Daten selbst gar nicht ins Gehege kommen, aber trotzdem irgendwelche Daten überschrieben werden. Selbst wenn der Benutzer weiß, was der Vendor macht, hilft ihm das auch nicht viel, denn auch dann würde er es ja überschreiben. Man müsste also als Benutzer die Vendor-Daten – etwa den NTP-Server oder irgendsowas – in die eigenen Einstellungen mit aufnehmen, damt das noch ausgeführt wird, dann funktioniert es aber nicht mehr, wenn der Vendor irgendwas ändern muss. Weil er dann seine Daten ändert, aber der Benutzer sie ja überschreibt.
Das Ding wird millionenfach verwendet, um – auch wichtige – Cloud-Server zu konfigurieren, und dann sind da solche Fallen drin, wonach auch kleine, harmlos erscheinende Änderungen das ganze Ding zusammenbrechen lassen, wenn die Kiste neu installiert wird, weil irgendein fieses Detail, irgend ein zu installierendes Paket oder irgendeine Konfigurationsdatei nicht installiert oder ein wichtiger Befehl nicht ausgeführt wird, den der Benutzer gar nicht sieht, weil nur der Provider weiß, warum das bei ihm nötig ist.
Und dann hat man plötzlich irgendeine Krise, weil Stromausfall, Angriff, Bug, Absturz, weiß der Teufel was war, und dann kommt die Neuinstallation trotz Puppet und so weiter nicht hoch, weil irgendein fieses Detail die Kiste blockiert, und diese Pappnasen denken überhaupt nicht drüber nach und arbeiten nach dem Motto „aber es gibt Leute, denen das so gefällt“.
Und schreiben es, obwohl sie es wissen, nicht mal so in die Dokumentation, dass man das auch eindeutig versteht. Ob nun diese Kollision oder eben, dass das Ding nicht, wie man glauben muss, nur für die Erstinstallation zuständig ist, sondern bei jedem Boot und Daten wieder überschreibt und zurücksetzt.
Das Ding wird fast überall eingesetzt und erscheint auf den ersten Blick sehr nützlich und praktisch, aber in der Tiefe lauern dann solche superidiotischen Fallen.
Und die Leute bauen nicht nur Mist, sie bauen ihn sogar bewusst und gewollt, weil sie irgendwelche Klapsvorstellungen haben und sich nur um ihre persönlichen Einsatzfälle kümmern, dokumentieren ihn nicht verständlich und sind dann auch noch pampig, wenn sie anderen damit großen Debugging-Aufwand (und im Krisenfall möglicherweise sehr großen Schaden) zufügen.
Und das sind die Phänomene, anhand deren man sieht, dass Open-Source immer schlechter wird, weil man immer öfter beobachten kann, dass da irgendwelche Leute mit nur überschaubarer Ahnung und sehr seltsamen Vorstellungen irgendwas zusammenmurksen, was ihnen da so gerade hilft. Und dann wie so trotzige Kinder mit „aber uns gefällt das so“ argumentieren, obwohl es andere gefährdet.
Absicht?
Ich frage mich, ob dahinter mehr steckt. Ob cloud-init verwendet wird, damit Provider Behörden schnell und einfach Zugang verschaffen können, etwa indem sie deren ssh public key noch als authorized eintragen.
Ich halte sowas für so richtig schlecht.
Und vor allem: Sehr gefährlich.
Und ähnliche Phönomene sind mir schon bei systemd und Network-Manager untergekommen, bei denen dann auch irgendein Prinzesschen irgendwas nicht verstanden hat, aber darauf besteht, alles besser zu wissen. Neulich habe ich beschrieben, dass der Network-Manager unter bestimmten Umständen falsche Routen für IPv6 einträgt und damit den Netzwerkverkehr derbe runterdrosselt, weil Pakete dann langsam geroutet statt schnell geswitched werden. Um dort auf Leute zu treffen, die nicht nur sehr seltsame Vorstellungen haben und sich für unfehlbar halten, sondern die die Netzwerkerei nicht verstanden haben – und dann das Netzwerkprogramm für Linux schreiben.
Aber: Wir sind jetzt divers. Gerecht. Emanzipiert. Tolerant. Quereinsteiger- und quotenfreundlich, und jeder ist sein eigener Fürst.
Und vor allem: „Quality is a myth“, wie man in den Gender Studies sagt.