skip to content
dz

Daniel Zenzes

5 min read Von nix eine Ahnung #3 EN

NixOS: Secrets im Git-Repo mit sops-nix

Mein Server holt sich seine Konfiguration jetzt selbst aus Git. Als Nächstes sind die Passwörter und Tokens dran, die die meisten Dienste brauchen und die nicht im Klartext in ein Repo gehören. Mit sops-nix liegen sie verschlüsselt direkt neben der Konfiguration, und nur der Server und ich können sie lesen.

1 min read EN

TIL: NixOS löscht Store-Pfade, die nur in Konfigdateien stehen

Heute Nacht hat sich mein Server plötzlich nicht mehr aktualisiert. Jede Stunde kam eine Notification mit dem Fehler No such file or directory. Der Pfad dahinter beginnt mit /nix/store/, und git fetch brach damit jedes Mal ab.

Schuld war ein Pfad in der /etc/nixos/.git/config. Beim ersten Klonen hat mein Service dort per git config core.sshCommand den Wert von ${pkgs.openssh}/bin/ssh eingetragen, also etwas wie /nix/store/98xy…-openssh-10.5p1/bin/ssh.

Nix legt jedes Paket in einem eigenen Verzeichnis unter /nix/store ab, mit einem Hash im Namen. Ein Update überschreibt nichts, die alte Version bleibt liegen, bis die Garbage Collection (nix-gc) aufräumt. Die löscht alles, worauf nichts mehr verweist: kein Systemstand, den Nix aufbewahrt, und kein anderes Paket. Dafür sieht Nix nur in die Dateien, die es selbst verwaltet. Die .git/config gehört nicht dazu, und mit einem Cronjob, einem Skript oder einer Dotfile wäre derselbe Fehler aufgetreten. Nach dem Update auf openssh 10.6p1 war die alte Version für Nix überflüssig, und um Mitternacht räumte die Garbage Collection auf.

Die Lösung: Ich setze GIT_SSH_COMMAND jetzt als environment am Service. Die Variable hat Vorrang vor core.sshCommand, und weil sie in der Systemkonfiguration steht, hält Nix das Paket am Leben:

systemd.services.nixos-auto-rebuild = {
  environment.GIT_SSH_COMMAND =
    "${pkgs.openssh}/bin/ssh -i /root/.ssh/infra-pull";
};

Einem Nix-Profi wäre das vermutlich nicht passiert. Ich habe danach meine Nix-Konfiguration nochmal gewissenhaft nach ähnlichen Stellen durchgesehen. In der Serie hatte ich es übrigens schon richtig: Dort steht nur ssh, das über path aufgelöst wird.

uptimekuma.co

Uptime Kuma

Gestern habe ich im Homelab und auf meinem Server Uptime Kuma installiert. Das Tool prüft in festen Abständen, ob Webseiten und Dienste erreichbar sind, und meldet sich, wenn nicht.

Und was soll ich sagen, es tut genau das, was es soll, sieht gut aus und bringt jede Menge Integrationen mit: Sehr viele Messenger, diverse Push Services und weit mehr Anbieter, als mir bisher bekannt waren. Bei mir überwachen sich Homelab und Server gegenseitig. Auf dem Server schaut es zusätzlich nach miniflux und linkding, im Homelab vor allem nach Home Assistant.

Welche Art von Check ich nutze, hängt vom Dienst ab: Wo es einen Health-Endpoint gibt, frage ich den per HTTP ab. Sonst reicht ein TCP-Check auf den Port, und für reine Geräte ein simpler Ping.

Auch bei Ready for Review läuft es jetzt, zusammen mit einem selbst gehosteten ntfy-Server, der die Alarme als Push-Nachricht verschickt. So kriegen wir mit, wenn etwas down ist.

Klare Empfehlung.

10 min read EN

Web Components robust schreiben: ein Guide aus dem Firefox-Quellcode

Web Components tun an ein paar Stellen nicht das, was man erwartet. Der constructor sieht ein Attribut einmal als "Ada" und einmal als null. Eine Komponente bleibt leer, ohne dass die Seite einen Fehler zeigt. Ein padding am <slot> bewirkt nichts. Das sind keine Browser-Bugs, das ist so spezifiziert, und es gilt in jedem Browser.

2026.stateofdevs.com

State of Devs 2026

Die Umfrage State of Devs 2026 beschreibt den Beruf gleich im ersten Satz des Überblicks so:

It is an uncertain time to be a developer.

Zwei Zahlen zeigen, was damit gemeint ist. Beim Thema KI liegen die Lager fast gleichauf: 49 % der Befragten sehen sie positiv, 42 % negativ. Fragt man aber, ob KI der eigenen psychischen Verfassung guttut, fällt das Urteil einseitiger aus: 46 % verneinen, nur 23 % bejahen.

Die Sorge um den Job hat ebenfalls Gründe. Ein Viertel der Befragten wurde schon einmal entlassen, knapp jeder Zehnte in den letzten neun Monaten, und mehr als ein Viertel rechnet damit, in den nächsten fünf Jahren den Beruf wechseln zu müssen.

Dass KI die Community spaltet, höre und merke ich seit Monaten. Genauso die Angst, dass Softwareentwicklung kein sicherer Beruf mehr ist. Neu ist nur, dass es jetzt in Zahlen dasteht.

Via @mauricerenck@mastodon.online.

1 min read

Linkding

Am Wochenende habe ich auf meinem Server Linkding installiert. Es macht genau eine Sache, Links verwalten, und die macht es gut.

1 min read

TIL: Kein Klick vor 6

Gestern kurz nach fünf, las ich eine Nachricht von einem guten Bekannten:

Hallo, könntest du bitte für Sophia abstimmen? Sie ist die Tochter von engen Freunden von mir, und dieser Sieg ist für ihre Familie sehr wichtig! Es geht um ein Stipendium für ihre Ausbildung. Vielen, vielen Dank im Voraus für deine Unterstützung! 🙏

Dazu ein Link, und ich hielt mich nicht an das, was ich selbst in der Familie oft genug gepredigt habe: Keine Links anklicken, egal von wem. Ich habe draufgetippt. Die Seite lud nicht. Was ich erst als Konfigurationsproblem abtat, geriet in Vergessenheit. So wichtig war mir die Wahl ja auch nicht.

Etwas später am Morgen kam dann seine Entschuldigung. Der Account meines Bekannten war "gehackt" worden, die Nachricht ging an seine ganze Kontaktliste. Also schaute ich mir die Nachricht nochmal an, und ja, viel offensichtlicher geht es nicht.

Was ich nicht zu sehen bekam, beschreibt Malwarebytes: Der Link führt nicht zu einer Abstimmung, sondern in WhatsApps eigene Funktion für verknüpfte Geräte. Man wird aufgefordert, einen Code einzugeben oder eine Verbindung zu bestätigen, und autorisiert damit ein fremdes Gerät auf dem eigenen Account. Ein Passwort wird nirgends abgefragt. Man richtet den Zugriff selbst ein.

Bei mir ist er nie so weit gekommen, und hier wollte ich dann doch wissen, wieso ich verschont wurde. Fündig wurde ich im Log von NextDNS: Blocked, Newly Registered Domain. Den Haken hatte ich vor Monaten gesetzt und seitdem nicht mehr angesehen. Er blockt jede Domain, die jünger als 30 Tage ist.

Genau von diesen jungen Domains lebt die Masche: eine frische Domain pro Welle, verbrannt nach ein paar Tagen, damit keine Blockliste sie kennt, wenn die erste Nachricht rausgeht.

Ich habe einfach nur Glück gehabt.

1 min read

KI-Assistenten-Assistent

Schon seit ein paar Wochen lese ich mehrere Artikel zum selben Thema: AI;DR gibt dem Muster einen Namen: Jemand lässt ein Sprachmodell eine Frage beantworten und reicht die Antwort ungelesen weiter, in einen Slack-Thread, ein Issue, einen Pull Request. Das erinnert mich ein wenig an Let me Google that for you, und auch wenn ich das schon verwendet habe, verstehe ich jeden, der das als sehr unhöflich empfindet. Hier fasst dontpastetheai.com das Richtige in einem einzigen Satz zusammen, No AI Fridays geht noch einen Schritt weiter und führt einen KI-freien Tag pro Woche ein.

Am schärfsten formuliert es Niklas Gruhn:

I don't need a meat proxy in between.

Sein Beispiel ist das Code-Review: Ticket in Claude Code kopieren, Ergebnis ungelesen als PR aufmachen, die Anmerkungen der Reviewer wieder reinkopieren, bis es durchgeht. Funktioniert, aber wer ist hier verantwortlich? Liegt die Arbeit dann nicht nur bei den Reviewern?

Ich kenne das andersherum. Jemand hinterlässt Anmerkungen an meinem Code und kann auf Nachfrage nicht sagen, was er meint. Eine Runde später kommt dann, die KI habe das falsch verstanden.

"Verstanden" hat da leider niemand etwas: Ein Modell setzt fort, was im Kontext steht, und wenn die Hälfte fehlt, kommt eben etwas anderes heraus. "Falsch verstanden" macht daraus ein Missgeschick, für das niemand etwas kann.

Ein Review ist aber kein Kommentar, sondern eine Zusage: Ich habe das gelesen, ich halte es für richtig, ich bin der Meinung, dass das produktiv gehen sollte. Diese Verbindlichkeit kann mir kein Modell abnehmen, egal welches. Wer sie nicht mitliefert, hätte mir auch gleich den Prompt schicken können.

lea.verou.me

Niemand braucht euren Dark-Mode-Schalter

Lea Verou argumentiert, dass beim Dark Mode zwei Zustände genügen, das Sichtbare und sein Gegenteil. "System" gehört unter die Haube, nicht in den Header.

Users cannot meaningfully express intent about problems they don't currently have.

Wer Dark Mode immer will, hat ihn längst im System stehen. Der Schalter muss ihn bloß nicht kaputtmachen.

Bei mir klickte sich ein Knopf im Header durch alle drei Zustände. Angefasst habe ich ihn nie. Jetzt sind es zwei, und der zweite Klick gibt wieder ans System ab.

Im Nachtrag braucht sie dann gar keinen Schalter mehr. So konsequent war ich heute nicht.

Via Stefan Judis' Web Weekly 199.

rmondello.com

TIL: Passkeys ziehen mit um

Seit ich über 1Passwords Omarchy-Investment geschrieben habe, evaluiere ich hier Alternativen. Passwörter sind in aller Regel schnell migriert. Bei den Passkeys hatte ich mich schon damit abgefunden, sie überall neu anzulegen, weil die nun mal am Anbieter hängen.

Tun sie seit iOS 26 nicht mehr. Ricky Mondello, der bei Apple an dem Thema arbeitet, erklärt das so:

Your credentials are yours to take and manage in whatever software you want.

Also ausprobiert. In 1Password auf Export getippt, den neuen Passwort-Manager als Ziel auswählen, Face ID, und keine Minute später lagen die Passkeys samt Schlüsselmaterial dort. Nichts daran war kompliziert. Möglich machen das zwei FIDO-Standards, Credential Exchange Format und Protocol: eins fürs Datenformat, eins für den verschlüsselten Transfer. Beide Seiten müssen sie unterstützen. Ich wusste nur nicht, dass es das schon gibt.

Am Mac geht das nicht immer, da können dann womöglich nur die Zugangsdaten übertragen werden. Ausgerechnet das Telefon, auf dem ich meine Tresore am wenigsten anfassen will, ist das einzige Gerät, das den Umzug kann.

3 min read Eleventy #3 EN

Eleventy: PostCSS und Tailwind CSS integrieren

In meinem Eleventy-Tutorial habe ich gezeigt, wie man mit Eleventy einen Blog aufsetzt und wie CSS in die Seiten kommt. Hier geht es einen Schritt weiter: Tailwind CSS im selben Build.

Der verbreitete Weg ist, beides nebeneinander laufen zu lassen. Die Tailwind-CLI beobachtet das Stylesheet, Eleventy beobachtet die Templates, und in der package.json hängen zwei Skripte an npm-run-all oder concurrently. Das funktioniert zuverlässig, bedeutet aber zwei Prozesse, die dasselbe Ausgabeverzeichnis befüllen, und Eleventy weiß nichts vom fertigen Stylesheet.

Ich lasse stattdessen Eleventy das CSS selbst bauen. Ein Hook erzeugt das Stylesheet, bevor das erste Template gerendert wird, und legt den fertigen Pfad als globale Daten ab. Ein Befehl, ein Prozess, und das Layout kennt den Dateinamen.

1 min read

Der Feed spricht jetzt Atom

Ich wollte einen alten Beitrag wieder nach vorne holen: PostCSS und Tailwind CSS in Eleventy integrieren. Der ist von 2022, zieht bis heute den meisten Suchtraffic und beschrieb ein Setup, das mit dem heutigen Eleventy nichts mehr zu tun hat. Also neu geschrieben. Nur: Wie bekommt das jemand mit, der ihn vor vier Jahren gelesen hat?

Ein frisches Veröffentlichungsdatum wäre gelogen, eine neue URL wirft die Verlinkungen von vier Jahren weg. Was ich brauche, ist ein zweites Datum: veröffentlicht 2022, überarbeitet 2026. Und genau da ist bei RSS Schluss.

RSS 2.0 wird seit 2009 nicht weiterentwickelt und kennt als Datum nur <pubDate>, mehr nicht. Reader identifizieren den Beitrag anhand seiner <guid> und zeigen ihn danach nie wieder als neu. Meistens will man genau das. Hier eben nicht.

Atom trennt beides: <published> fürs Erscheinen, <updated> für die letzte Änderung, dazu eine stabile <id>. Kein Duplikat im Reader, aber wer <updated> auswertet, schiebt den Beitrag wieder nach oben. Ein Feld, das seit 2005 im Standard steht und mir jetzt die ganze Arbeit abnimmt.

Die Entscheidung hatte mir ohnehin jemand abgenommen, nämlich ich selbst vor Jahren. Im <head> dieser Seite stand die ganze Zeit:

<link rel="alternate" type="application/atom+xml" href="/feed.xml" />

Ausgeliefert wurde darunter aber RSS 2.0. Jetzt stimmt es: /feed.xml liefert Atom 1.0, die alten <guid>-Werte sind unverändert als <id> übernommen, und der aufgefrischte Tailwind-Beitrag trägt ein <updated> von heute.

Wer den RSS Atom-Feed abonniert hat, sollte keinen Unterschied bemerken. Und falls meine Änderung schief geht und der ganze Blog als ungelesen aufpoppt: Formatwechsel, einmalig, dieser Beitrag ist die Erklärung.