Die Zeile stand an einem Freitagnachmittag im Egress-Log, zwischen zweihundert unauffälligen anderen:
DENY job-4711 → collect.some-analytics.io:443
Ein Agentenlauf hatte eine Abhängigkeit installiert, und irgendwo in deren Installationsskript stand ein Aufruf nach draußen. Kein Angriff, keine kompromittierte Registry. Eine Bibliothek, die beim Einrichten Nutzungsdaten meldet, so wie sie das seit Jahren tut. Das Interessante war nicht, dass sie es versucht hat. Das Interessante war, dass niemand sie darum gebeten hatte, dass die Verbindung nicht zustande kam und dass wir am Montag lesen konnten, dass sie es versucht hatte.
Wir erzählen das gern, weil es zeigt, worum es bei Agentensicherheit tatsächlich geht. Die Frage ist nie, ob das Modell böse Absichten hat. Die Frage ist, was passieren kann, wenn irgendetwas in dieser Kette — das Modell, ein Paket, ein Skript in einem Repository, ein Textschnipsel in einem Ticket — etwas tut, das niemand vorgesehen hat.
Vertrauen ist keine Kontrolle
Der übliche erste Reflex ist, die Vertrauensfrage zu stellen: Können wir dem Modell vertrauen? Ist es zuverlässig genug, um es unbeaufsichtigt auf ein Repository zu lassen?
Die Frage klingt vernünftig und führt trotzdem in die falsche Richtung, denn sie hat keine überprüfbare Antwort. Ein Agent, der in tausend Läufen nichts Unerwartetes getan hat, ist keine Garantie für den tausendundersten — schon deshalb nicht, weil sein Verhalten von Eingaben abhängt, die aus dem Internet stammen. Der Text in einem Issue ist Eingabe. Die README einer Abhängigkeit ist Eingabe. Die Fehlermeldung eines Build-Tools ist Eingabe.
Vertrauen skaliert nicht. Grenzen schon. Eine Grenze musst du einmal ziehen, und danach gilt sie für jeden Lauf, für jedes Modell, für jede Version einer Abhängigkeit, ohne dass jemand sie erneut bewerten muss. Sie ist außerdem das Einzige, worüber du gegenüber einem Kunden eine Aussage treffen kannst, die nicht “wir haben gute Erfahrungen gemacht” lautet.
Vier Grenzen, die tatsächlich etwas ändern
Ein Container ohne Docker-Socket. Der Agent läuft in einem Container — das ist schnell gesagt und meistens die Stelle, an der es aufhört. Der Unterschied liegt darin, was in diesen Container hineingereicht wird. Ein durchgereichter Docker-Socket macht die Isolation zur Dekoration: Wer den Socket ansprechen kann, startet einen neuen Container mit dem Dateisystem des Hosts darin und ist damit außerhalb. Dasselbe gilt für gemountete Host-Verzeichnisse, für privilegierte Modi und für Netzwerkzugriff auf die Steuerungsebene. Der Lauf braucht ein Arbeitsverzeichnis, eine Laufzeitumgebung und sonst nichts. Am Ende wird der Container weggeworfen, und mit ihm alles, was ein Skript darin abgelegt hat.
Eine Allowlist statt offenem Egress. Ausgehender Verkehr ist die Grenze, die am häufigsten fehlt, weil sie nirgends weh tut, solange sie offen ist. Ein Agentenlauf braucht die Paketregistry, den Git-Host, die API des Modellanbieters. Das ist eine kurze Liste, und alles außerhalb davon ist eine Anfrage, die niemand bestellt hat. Genau so ist die Zeile oben entstanden. Wer stattdessen alles hinauslässt und nur den eingehenden Verkehr regelt, hat für den Fall, dass ein Secret abfließt, keinen zweiten Riegel.
Secrets pro Repository. Es ist bequem, einen Satz Zugangsdaten für alle Läufe zu haben. Nur ist die Reichweite eines Fehlers dann die Summe aller Projekte. Ein Token, das für genau ein Repository gilt, für die Dauer eines Laufs, mit Schreibrechten auf Branches und nicht mehr, verwandelt einen möglichen Totalschaden in einen begrenzten Vorfall. Der Aufwand dafür ist Konfiguration, einmalig. Der Unterschied im Ernstfall ist die ganze Bandbreite zwischen einem Branch, den man löscht, und einem Wochenende, an dem alle Schlüssel getauscht werden.
Ein Protokoll der Ablehnungen, das jemand liest. Eine abgelehnte Verbindung, die nirgends auftaucht, ist eine verpasste Information. Und ein Log, das niemand öffnet, ist dasselbe in aufwendiger. Bei uns ist das eine kurze wöchentliche Durchsicht: Welche Ziele wurden abgelehnt, welche davon sind neu? Meistens ist es Telemetrie, manchmal ein CDN, das eine Abhängigkeit erst zur Laufzeit nachlädt. Beides ist wertvoll zu wissen, bevor es jemand anders herausfindet. Wichtig ist nur, dass die Liste kurz genug bleibt, um sie wirklich zu lesen — deshalb lohnt es sich, bekannte, harmlose Ziele ausdrücklich zu markieren, statt sie im Rauschen mitlaufen zu lassen.
Die stärkste Grenze ist die, die du nicht implementierst
Der Punkt, der in solchen Aufzählungen fast immer fehlt, ist zugleich der wirksamste: Die sicherste gefährliche Operation ist die, die es nicht gibt.
Wir haben das an der Stelle gelernt, an der ein Agent mit Git arbeitet. Man kann ihm die Kommandozeile geben und ihm in der Anleitung erklären, dass er niemals mit Gewalt auf main pushen und keine Branches löschen soll. Das funktioniert erstaunlich oft. Es funktioniert nicht immer, und es funktioniert vor allem nicht überprüfbar — eine Anweisung in einem Prompt ist eine Bitte, kein Mechanismus. Die Alternative ist eine schmale Schnittstelle: eine Funktion, die einen Branch anlegt und darauf committet, eine, die einen Pull Request eröffnet. Kein Löschen, kein Überschreiben von Historie, weil es diese Funktionen im Werkzeugkasten schlicht nicht gibt.
Danach ist die Frage, ob das Modell zum Löschen zu überreden wäre, gegenstandslos. Code, der Löschen nicht ausdrücken kann, lässt sich nicht dazu überreden. Kein Prompt, keine noch so geschickt formulierte Zeile in einem Issue, kein Missverständnis über den Zustand des Repositories führt zu einer Operation, für die es keinen Aufruf gibt.
Dieselbe Überlegung trägt weiter, als man zuerst denkt. Ein Agent, der Deployments auslösen darf, braucht selten mehr als den Anstoß einer Pipeline, die selbst ihre Regeln kennt. Ein Agent, der Daten liest, braucht selten eine Verbindung, die auch schreiben kann. Jedes Mal, wenn du eine Fähigkeit weglässt statt sie zu regulieren, sparst du dir eine Regel, die später jemand prüfen müsste.
Was das für ein Gespräch mit einem Kunden bedeutet
Die Frage “Wie stellt ihr sicher, dass euer Agent nichts kaputt macht?” lässt sich mit Zuversicht beantworten oder mit Architektur. Zuversicht klingt in einem Meeting oft besser und hält keiner Nachfrage stand.
Die belastbare Antwort ist eine Aufzählung von Dingen, die technisch nicht möglich sind: Der Lauf kann den Host nicht sehen. Er kann keine Adresse erreichen, die nicht auf der Liste steht. Sein Token gilt für dieses eine Repository und für die nächste Stunde. Er kann keine Historie überschreiben, weil im Werkzeugkasten kein Befehl dafür liegt. Und wenn etwas an eine dieser Grenzen stößt, steht es in einem Protokoll, das montags jemand durchgeht.
Nichts davon setzt voraus, dass das Modell sich benimmt. Genau das ist der Punkt.