Zum Inhalt springen

Blog ·

Ein Geheimnis, das genau einmal reist

Eine Überwachungsprüfung braucht am anderen Ende ein Passwort. So erreicht es genau eine Sonde, für genau einen Versuch, ohne je in einem Log zu landen.

Von Provibr

Manche Überwachungsprüfungen brauchen Zugangsdaten. Man kann einen PostgreSQL-Server nicht fragen, ob es ihm gut geht, ohne sich anzumelden, und eine Redis-Instanz mit gesetztem Passwort antwortet nichts Brauchbares, bevor man es geschickt hat. Eine Überwachungsregel muss also ein Geheimnis von uns zu derjenigen Sonde oder demjenigen Agenten bringen, der den nächsten Versuch zufällig übernimmt.

Die billige Fassung davon ist eine Spalte. Das Passwort in die Regel schreiben, beim Versand lesen, mitschicken. Diese Fassung ist auf eine Weise falsch, die erst dreizehn Monate später sichtbar wird, und dies ist die Form, die wir stattdessen gebaut haben.

Die zwei Wege, die es schon gab, und warum keiner taugt

Ein Überwachungsbefehl erreicht eine Quelle über zwei Stellen, und beide sind ungeeignet.

Die Revisionstabelle der Regel ist bewusst unveränderlich, was für eine Revision genau richtig und für Zugangsdaten genau falsch ist: niemand kann ein Passwort rotieren, das in einer Zeile steht, die sich nicht ändern lässt, ohne von jeder Regel, die es verwendet hat, eine neue Revision zu erzeugen.

Die Befehlszeile ist schlimmer, denn sie ist eine Prüfzeile und reist auf ihrem Weg nach draußen über den Nachrichtenbus. Unsere eigenen Projektnotizen sagen seit einem Jahr, dass eine Befehlsprüfzeile Feldnamen enthält und niemals Werte. Ein Passwort, das dort einmal landet, steht die gesamte Aufbewahrungsfrist darin, lesbar für jeden mit Datenbankzugriff und in jede Sicherung kopiert, die in jener Nacht lief.

Es gibt außerdem einen flüchtigen Seitenkanal für genau solche Fälle, genutzt, wenn jemand eine Maschine bestellt und ein Rootpasswort den Agenten erreichen muss, ohne gespeichert zu werden. Auch der passt hier nicht. Dieser Kanal existiert für Geheimnisse, die der Erzeuger eines Befehls bereits kennt, und hier ist der Erzeuger ein Planer, der Minuten früher läuft und noch nicht weiß, welche Quelle die Aufgabe übernimmt.

Die vier Protokollnachrichten dieser Runde haben deshalb kein Benutzernamenfeld und kein Passwortfeld. Es ist nicht so, dass die Felder ungenutzt wären; es gibt sie nicht.

Versiegelt im Augenblick des Sendens

Der Umschlag entsteht im Sitzungsprozess, unmittelbar bevor die Nachricht gebaut wird, einmal je Versuch. Das ist keine Ordnungsliebe. Es ist der einzige Punkt im System, an dem alle vier Dinge gleichzeitig bekannt sind, die er binden muss: welche Quelle am anderen Ende dieser Sitzung sitzt, welcher Versuch dies ist, zu welcher Revision der Regel er gehört und wann er verfällt.

Die Folge ist, dass er nirgendwo sonst sein kann, und das ist prüfbar statt behauptbar. Ein Test pflanzt ein bekanntes Passwort ein, fährt den echten Pfad und sucht danach in der Befehlsnutzlast, in den Ereigniszeilen, in der gespeicherten Prüfkonfiguration, im Nachrichtenbus, in den rohen Protokollbytes sowohl als UTF-8 als auch als Base64 und im Gateway-Protokoll. Er verlangt null Treffer in allen sechs. Eine Liste von Behauptungen darüber, wo ein Geheimnis nicht hingelangt, ist nichts wert; eine Suche ist etwas wert.

Der Klartext existiert die Länge eines Ausdrucks. Eine Funktion erzeugt ihn, die nächste verbraucht ihn, danach gibt es keine Referenz mehr. JavaScript kann keinen Puffer löschen, also bleibt nur, nie einen zu behalten. Auf der anderen Seite, in Rust, landet er in einem Typ, der sich beim Aufräumen selbst nullt.

Der Aufbau

Ein flüchtiges X25519-Schlüsselpaar je Versuch, ein Diffie-Hellman gegen den öffentlichen Schlüssel genau einer Quelle, HKDF-SHA256 zur Ableitung des Chiffrierschlüssels und AES-256-GCM über den Klartext. Über die Leitung gehen der flüchtige öffentliche Schlüssel sowie Nonce, Chiffrat und Prüfsumme.

Die beiden öffentlichen Schlüssel wandern in das Salz der HKDF, damit der abgeleitete Schlüssel an dieses Paar gebunden ist und nicht an ein gemeinsames Geheimnis, das im Prinzip aus einem anderen Paar entstehen könnte. Derselbe Gedanke wie der Schlüsselplan eines gängigen hybriden Verschlüsselungsverfahrens, mit der Vereinfachung, die hier ehrlich ist: es gibt je Versuch einen frischen flüchtigen Schlüssel, also wird kein Schlüssel zweimal benutzt.

Vier Felder gehen in die zusätzlich authentifizierten Daten:

FeldWas es verhindert
VersuchskennungDieselbe Quelle, die einen Umschlag für eine spätere Messung wiederverwendet
RegelrevisionskennungEin Umschlag, der unter eine andere Prüfung geklebt wird
FristEin Umschlag aus einer Paketaufzeichnung oder einem Speicherabzug, der später noch taugt
VerwendungszweckEin PostgreSQL-Zugangsdatum, das einer Redis-Prüfung vorgesetzt wird

Die Identität der Quelle steht nicht in dieser Liste, weil sie es nicht muss: es wird auf genau einen öffentlichen Schlüssel versiegelt, eine andere Quelle bekommt ihn also ohnehin nicht auf.

Längenpräfixe, weil ein Trennzeichen mehrdeutig ist

Die zusätzlichen Daten werden mit der Länge jedes Feldes davor gebaut statt mit einem Trennzeichen dazwischen. Das liest sich nach Erbsenzählerei, bis man die Alternative aufschreibt.

Mit a:b als Format ergeben das Paar ("x", "y:z") und das Paar ("x:y", "z") identische Bytes. Die Bindung, die diese Daten herstellen sollen, unterscheidet dann zwei verschiedene Lagen nicht mehr, und wer einen Teil eines Feldes kontrolliert, kann die Grenze verschieben. Vier Felder mit vier Byte Länge davor kennen dieses Problem nicht. Beide Seiten bauen sie Byte für Byte gleich, mit einem geteilten Testvektor, sodass eine Abweichung ein roter Test ist und kein Umschlag, den niemand mehr öffnet.

Warum nicht die Chiffre, die wir schon nutzen

Der Zugangsdatentresor des Agenten nutzt XChaCha20-Poly1305, und derselbe Aufbau hier ergäbe eine Antwort auf die Frage „wie verschlüsseln wir ein Geheimnis“ statt zwei. Wir haben gemessen, und es geht nicht:

text
node v24.20.0   crypto.getCiphers() enthält "chacha20-poly1305"
bun  1.4.0      crypto.getCiphers() liefert 28 Namen, kein chacha
                createCipheriv("chacha20-poly1305", ...) -> "Unknown cipher"

Die Produktion läuft auf Node, dort würde es also funktionieren. Aber das Testgerüst dieses Repositorys ist bun, und ein AEAD, das das Gerüst nicht ausführen kann, ist ein AEAD, dessen Hin und Zurück nie bewiesen wird. Die einzige Kontrolle über den Umschlag stünde dann in einem übersprungenen Test, und das ist genau die Sorte leerlaufend grüner Messung, deren Vermeidung uns viel Mühe wert ist.

Die Alternative war, das fehlende Stück von Hand dazuzuschreiben. Das wurde tatsächlich getan, es funktionierte gegen den veröffentlichten Testvektor, und danach wurde es entfernt. Kryptografischer Code, der nur existiert, um ein Testgerüst zu umgehen, gehört nicht in ein Produktrepository.

Also AES-256-GCM, dasselbe AEAD, das dieses Geheimnis bereits im Ruhezustand verschlüsselt, in beiden Laufzeiten vorhanden und im Abhängigkeitsbaum des Agenten schon enthalten. Die 96-Bit-Nonce, wegen der man sonst zu XChaCha greift, ist hier kein Einwand: XChaCha existiert, damit man Nonces unter einem langlebigen Schlüssel zufällig wählen darf. Es gibt keinen langlebigen Schlüssel. Jeder Umschlag hat einen frischen flüchtigen, also wird jeder AEAD-Schlüssel genau einmal verwendet, und eine wiederholte Nonce wäre keine zweite Nachricht unter demselben Schlüssel.

Wo die Quelle ihre Hälfte aufbewahrt

Der private Schlüssel liegt neben der Identität des Agenten oder der Sonde, in einer Datei aus zweiunddreißig rohen Bytes mit Modus 0600, neben dem privaten Schlüssel des Clientzertifikats. Nicht im Zugangsdatentresor, und die drei Gründe sind das Aufschreiben wert, weil sie die eigentliche Entscheidung waren.

Ein Entzug löscht ihn umsonst: der Entzug löscht dieses Verzeichnis bereits, und ein anderswo abgelegter Schlüssel bräuchte eine eigene Aufräumregel, die die erste Runde, die sie vergisst, auf einem entzogenen Host zurücklässt. Er gehört zu einer Anmeldung, und die Plattform bewahrt die öffentliche Hälfte neben dem Zertifikat auf, eine neue Anmeldung sollte also ein neues Paar erzeugen, was sie tut, weil das Verzeichnis dann leer ist. Und der Tresor ist nicht immer verfügbar: er wird mit Material aus der Lizenzdatei entsperrt, und eine Überwachungssonde hat keine Lizenz und damit keinen Tresor. Den Schlüssel dorthin zu legen, erzwänge einen zweiten Mechanismus für Sonden, und dann gäbe es zwei Antworten auf eine Frage.

Er wird bewusst auch nicht im Speicher gehalten, was für eine Sonde, die nichts ablegt, verlockend ist. Die Plattform bewahrt die öffentliche Hälfte auf, nach einem Neustart passte sie also nicht mehr, und jeder Umschlag bliebe verschlossen, bis eine Betreiberin neu anmeldet. Ein Überwachungsstandort, der nach einem Neustart still keine authentifizierten Prüfungen mehr macht, ist genau die Art Störung, die niemand bemerkt.

Niemals ein Rückfall

Hat eine Quelle keinen Schlüssel, läuft die Prüfung nicht. Sie meldet einen Konfigurationsfehler, der den Vermittler als nicht verfügbar benennt, und das ist das gesamte Verhalten. Es gibt keinen Rückfall auf Klartext und, wichtiger, keinen irreführenden Ausfallstatus für das Ziel, denn eine Prüfung, die nicht messbar ist, darf nie wie ein kaputter Dienst aussehen. Das ist ein Alarm, der jemanden um drei Uhr nachts zur falschen Maschine schickt.

Was wir aufschreiben würden

  1. Eine Prüfzeile ist kein Zustellweg. Was dort geschrieben wird, steht die ganze Aufbewahrungsfrist darin, in jeder Sicherung.
  2. Fügen Sie das Feld nicht hinzu. Eine Protokollnachricht ohne Passwortfeld kann keines über einen Codepfad verlieren, den jemand zu prüfen vergisst.
  3. Versiegeln Sie dort, wo die Bindungen bekannt sind. Weiß der Erzeuger einer Aufgabe noch nicht, wer sie ausführt, kann das Geheimnis nicht mit der Aufgabe entstehen.
  4. Beweisen Sie Abwesenheit durch Suchen, nicht durch Behaupten. Pflanzen Sie einen Wert ein und jagen Sie ihn in jeder Serialisierung, Base64 eingeschlossen.
  5. Setzen Sie Längenpräfixe vor Ihre authentifizierten Daten. Ein Trennzeichen lässt zwei verschiedene Tupel dieselben Bytes ergeben.
  6. Wählen Sie die Grundfunktion, die Ihr Testgerüst ausführen kann. Ein stärkerer Aufbau, der nie ausgeführt wird, ist in der Praxis schwächer als ein guter, der es wird.
  7. Ein Schlüssel, der eine Identität ist, gehört zur Identität. Dann räumt der Entzug ihn auf und eine erneute Anmeldung ersetzt ihn, ohne eine Regel, die sich jemand merken muss.

Zur Klarheit darüber, was das ist: die Überwachung liegt hinter einem standardmäßig ausgeschalteten Ausrollschalter, davon ist heute also nichts käuflich. Zwei Dinge sind unbewiesen. Der passwortgestützte Anmeldeaustausch wurde nur gegen die Referenzumsetzung der Protokollbibliothek gemessen und nicht gegen einen echten Server. Und die Rust-Sondenbinärdatei wurde in dieser Runde nicht über die Leitung gefahren.

Aus der Plattformarbeit von Provibr. Warum eine Sonde jede empfangene Nachricht als nicht vertrauenswürdig behandelt, steht in eine Sonde, die sich zu nichts überreden lässt, und warum die Zugangsdaten der eigenen Panels eines Kunden uns nie erreichen, steht in wo Panelzugangsdaten hingehören.

Fangen Sie mit einem Host und einem Agenten an

Konto erstellen, den Agenten mit einem einzigen Befehl installieren und Ihren ersten Proxmox-Host verbinden. Die Testphase läuft 14 Tage und fragt nicht nach Zahlungsdaten.