Tor als CRD: ein eigener Operator statt Sidecar-Bastelei

Warum ich den inoffiziellen tor-controller durch einen eigenen, in Rust geschriebenen Operator ersetzt habe — ein gemeinsamer Tor-Daemon pro Namespace, Onion-Services und Onion-Proxies dynamisch über den Control-Port, plus kuratierte obfs4-Bridges

Tor-Konnektivität im Cluster hatte ich bisher zusammengesteckt: pro Pod ein client-only tor plus ein oder zwei socat-Sidecars, die lokale TCP-Ports in Onion-Services übersetzten — genau das Setup, das ich im IRC-Bouncer-Beitrag beschrieben habe. Das funktioniert, skaliert aber nicht als Muster: Jeder Workload, der eine .onion erreichen oder selbst eine anbieten wollte, brachte seinen eigenen Tor-Prozess und seine eigene Klebeschicht mit. Also habe ich das Ganze zu dem gemacht, was es im Lab sein sollte: einer Custom Resource. tor-operator ist ein eigener, in Rust geschriebener Kubernetes-Operator, der Tor-Konnektivität in beide Richtungen als CRD anbietet.

ZeroClaw 0.7.5 → 0.8.0: Das Upgrade, das den `<tool_call>`-Spam beendet hat

Eine schwierige, brechende Pre-1.0-Migration: Schema V3, Multi-Agent-Runtime und ein neues On-Disk-Layout — angetrieben von einem einzigen, hartnäckigen Bug. Mein KI-Agent kippte rohe <tool_call>-Blöcke in Matrix, die nie ausgeführt wurden. Warum v0.8.0 der eigentliche Fix war, was alles dabei zerbrach und welche drei Fehler erst zur Laufzeit auftauchten.

In meinem Lab läuft seit Wochen ein KI-Agent mit — ZeroClaw , im Chat nur Claw —, der die Cluster überwacht, mir auf Matrix Bescheid gibt und Reparaturen als Radicle-Patches vorschlägt statt sie selbst durchzudrücken. Über genau einen Bug bin ich dabei so lange gestolpert, dass er mich am Ende zu einer der unangenehmsten Migrationen des ganzen Setups getrieben hat: dem Sprung von v0.7.5 auf v0.8.0. Das ist ein brechendes Pre-1.0-Release — ein von Grund auf neu geschriebenes Config-Schema, eine Multi-Agent-Runtime und ein neues On-Disk-Layout. Dieser Beitrag erzählt, warum ich diese Migration trotzdem unbedingt wollte, was dabei alles zerbrochen ist und welche Fehler mir erst zur Laufzeit um die Ohren geflogen sind.

Vault als PKI rausgeworfen — eine Offline-CA-Hierarchie mit cert-manager

Vault als interne Zertifizierungsstelle war bequem und ein Single Point of Failure zugleich: Läuft Vault nicht, wird kein Cert mehr ausgestellt. Ich habe die PKI auf eine klassische Offline-Hierarchie umgestellt — Root und Intermediate-Key liegen air-gapped, eine kurzlebige Sub-CA lebt als cert-manager-ClusterIssuer im Cluster, und Name Constraints erzwingen, dass kein Blatt je außerhalb meiner internen Domains signiert wird. Ein staged Cutover ohne Trust-Lücke inklusive.

Lange hat Vault in meinem Lab die internen Zertifikate ausgestellt — eine PKI-Engine, ein cert-manager-ClusterIssuer namens vault, fertig. Bequem. Und ein Single Point of Failure mit Ansage: Ist Vault sealed, abgelaufen oder beim Bootstrap, stellt niemand mehr ein Cert aus. Genau dieses Henne-Ei hat mich schon einmal in eine Break-Glass-Recovery gezwungen. Also habe ich die PKI von Vault gelöst und auf das gestellt, wofür X.509 eigentlich gedacht ist: eine Offline-Hierarchie, deren teuerste Schlüssel nie online sind.

Drei Schichten für ein Secret — von pass über SOPS und Vault in den Pod

In einem öffentlichen GitOps-Repo darf nie ein Klartext-Secret liegen — und trotzdem muss jeder Workload an seine Passwörter kommen. Mein Lab löst das in drei klar getrennten Schichten: SOPS+PGP/age für die paar Bootstrap-Secrets, die vor Vault existieren müssen; Vault KV als Langzeitspeicher für alles Anwendungsnahe; und der External Secrets Operator, der daraus zur Laufzeit Kubernetes-Secrets materialisiert. Eine Tour durch den Lebensweg eines Geheimnisses.

Mein Lab ist ein öffentliches GitOps-Repo — über meinen Radicle -Seed-Node seed.this-is-fine.io kann es jeder klonen. Daraus folgt eine harte Regel: kein Klartext-Secret, niemals, nirgends in Git. Und trotzdem braucht jeder Workload im Cluster seine Passwörter, API-Tokens und Signing-Keys. Diesen Widerspruch löse ich in drei Schichten, von denen jede genau eine Aufgabe hat.

Im Beitrag über die pre-commit-Hooks ging es darum, wie ich verhindere, dass ein Secret versehentlich in Git landet. Hier geht es um den geplanten Weg: wie ein Secret absichtlich von meiner Maschine bis in einen Pod fließt, ohne je unverschlüsselt das Repo zu berühren.

Kein Klartext-Secret kommt durch — pre-commit, gitleaks und SOPS

Ein GitOps-Repo ist öffentlich, sobald man es vergisst. Drei Hooks sorgen dafür, dass ich gar nicht erst ein unverschlüsseltes Secret committen kann: gitleaks scannt auf Muster, ein SOPS-Hook verweigert Klartext in geschützten Pfaden, und ein lokaler Decrypt-Check stellt sicher, dass jede verschlüsselte Datei sauber entschlüsselt und alle Empfänger trägt.

Mein Lab-Repo ist öffentlich — abrufbar über meinen Radicle -Seed-Node seed.this-is-fine.io. Genau deshalb darf da nie ein Klartext-Secret hineinrutschen. Nicht „sollte nicht", sondern kann nicht: Drei pre-commit -Hooks fangen den Fehler ab, bevor er ein Commit-Objekt wird. Das ist die billigste Versicherung, die ich kenne — ein paar Zeilen YAML gegen einen Leak, den man nie ganz zurückholt.