# Audit-Eintrag für Agentenaktionen: Ein praktisches Kontrollmodell

Agentenaktionen brauchen Einträge, die ihre Berechtigung beschreiben, nicht nur ihre Aktivität. Eine Zeile wie `POST /deploy returned 200` verrät einer untersuchenden Person nicht, ob der richtige Prozess die Anfrage gestellt hat, ob eine Person sie erlaubt hat, welches System sie empfing oder ob ein späterer Widerruf überhaupt etwas gestoppt hat.

Die praktische Einheit ist ein unveränderlicher Aktions-Eintrag, der mit Sitzungs-, Genehmigungs- und Widerrufsereignissen verbunden ist. Halte den Aktions-Eintrag klein genug, damit er sich durchsuchen und prüfen lässt, aber präzise genug, dass eine müde Entwicklerin oder ein müder Entwickler einen strittigen Aufruf rekonstruieren kann, ohne die Lücken mit einer erfundenen Geschichte füllen zu müssen.

## Ein Audit-Eintrag muss einen autorisierten Versuch beschreiben

Ein **Audit-Eintrag für eine Agentenaktion** erfasst einen versuchten Eingriff in ein externes System und den Berechtigungskontext, der in diesem Moment vorlag. Eine Aktion kann scheitern, bevor sie das Netzwerk erreicht, während der Übertragung fehlschlagen, vom Ziel abgelehnt werden oder erfolgreich abgeschlossen werden. Alle vier Fälle gehören in den Audit-Trail.

Teams protokollieren oft eine von zwei unzureichenden Informationen. Sie speichern ein Chatprotokoll, das die Absicht beschreibt, aber nicht die Ausführung. Oder sie speichern den rohen Request-Verkehr, der zu viele Geheimnisse enthält und trotzdem die menschliche Entscheidung auslässt, durch die der Aufruf erlaubt wurde. Keiner dieser Einträge beantwortet, ob ein bestimmter Akteur zu einer bestimmten Operation berechtigt war.

Behandle diese Dinge als getrennte Objekte:

- Eine **Sitzung** identifiziert einen laufenden Agentenprozess und seine Lebensdauer.
- Eine **Genehmigung** erfasst eine menschliche Entscheidung mit festgelegtem Umfang.
- Eine **Aktion** erfasst eine angeforderte externe Operation und ihr Ergebnis.
- Ein **Widerruf** beendet eine Sitzung oder Berechtigung zu einem genau bestimmten Zeitpunkt.

Diese Unterscheidung ist während eines Vorfalls entscheidend. Wenn eine Person eine Sitzung um 14:03 Uhr widerruft, musst du jede Aktion vor 14:03 Uhr, jeden Versuch danach und die lokale Entscheidung sehen können, die die späteren Versuche ablehnte. Eine einzige veränderliche Zeile mit der Aufschrift «Sitzung genehmigt: nein» zerstört diese Geschichte.

Mach das Datenmodell nicht davon abhängig, ob die Aktion erfolgreich war. Die Anfrage selbst kann auf böswillige Absicht, einen Konfigurationsfehler oder einen Agenten hinweisen, der seine Aufgabe missverstanden hat. Fehlgeschlagene Aufrufe liefern oft die ersten brauchbaren Hinweise.

## Der Akteur ist ein Prozess, kein freundlicher Agentenname

Erfasse den Prozess, der die Aktion tatsächlich angefordert hat. Ein Modellname, Workspace-Name oder Agenten-Spitzname kann das Lesen eines Berichts erleichtern, identifiziert aber nicht eindeutig die ausführbare Datei, die zu diesem Zeitpunkt die Verbindung hielt.

Ein brauchbares Akteurobjekt enthält eine interne Sitzungskennung, die Prozess-ID, den Pfad der ausführbaren Datei, den Startzeitpunkt und die Code-Signatur, sofern das Betriebssystem diese Information liefert. Erfasse auch die ID und die ausführbare Datei des übergeordneten Prozesses, wenn du sie zuverlässig sammeln kannst. Die Beziehung zum übergeordneten Prozess erklärt oft, ob ein Terminal den Agenten gestartet hat, eine Editor-Erweiterung ihn erzeugte oder ein unerwarteter Helfer beteiligt war.

Verwende eine Momentaufnahme der Prozessidentität, statt den Prozess später aufzulösen. Prozess-IDs werden wiederverwendet. Pfade können sich ändern. Eine spätere Abfrage kann eine fehlende Beobachtung nicht reparieren.

```json
{
  "actor": {
    "session_id": "ses_01J8Q1F9K9Z3",
    "pid": 48217,
    "started_at": "2025-02-18T21:14:06Z",
    "executable": "/usr/local/bin/agent-runner",
    "signing_authority": "Developer ID Application: Example Developer",
    "parent_pid": 48091,
    "parent_executable": "/Applications/Terminal.app"
  }
}
```

Der Wert in `signing_authority` sollte aus der Signaturprüfung der Plattform stammen, nicht aus einer Zeichenfolge, die der Agent selbst liefert. Ein Agent kann sich `claude-code`, `deploy-helper` oder `trusted-agent` nennen. Ein Name ist nur Dekoration, solange das Betriebssystem ihn nicht mit einer Identität der ausführbaren Datei verknüpft.

Mach aus der Prozessidentität keine falsche Aussage über die Urheberschaft. Eine signierte Binärdatei kann nach einer Prompt-Injection, durch ein kompromittiertes Plugin oder aufgrund einer unbedachten Anweisung problematisch handeln. Das Feld zeigt, welche Code-Autorität die Anfrage initiiert hat. Es bestätigt nicht, dass die Anfrage sinnvoll war.

RFC 5424, die Spezifikation des Syslog-Protokolls, trennt Header-Felder wie Anwendungsname und Prozess-ID von strukturierten Daten. Diese Trennung ist auch für Agenten-Einträge sinnvoll. Lege stabile Identität und Zeitangaben in eigene Felder, die Maschinen abfragen können. Variablen Kontext wie Repository oder Aufgabenreferenz gehört in ein Objekt mit Namespace. Wenn alles in einer einzigen Nachrichtenzeichenfolge landet, wird jede spätere Untersuchung zu Textanalyse.

## Ein Ziel braucht sowohl eine Adresse als auch eine Bedeutung

Erfasse, wohin eine Aktion ging und welche Ressource oder welchen Befehl sie beeinflussen sollte. Diese Fakten hängen zusammen, sind aber nicht austauschbar.

Bei HTTP kann das Netzwerkziel `api.example.internal` sein, während die eigentliche Operation `POST /v1/releases/{release_id}/promote` lautet. Speichere Host, Port, Protokoll, HTTP-Methode und eine Routenvorlage. Ergänze eine Kennung des Zielsystems, die dein Team kontrolliert, etwa `release-service-prod`. Die Kennung bleibt bei einer Hostnamen-Migration erhalten; der Hostname hilft bei der Diagnose der Anfrage, die den Rechner tatsächlich verlassen hat.

Bei SSH solltest du Host-Alias oder Hostnamen, Port, Fingerabdruck des Host-Schlüssels oder einen Verweis auf den Known-Hosts-Eintrag sowie die Bezeichnung des entfernten Kontos speichern, sofern diese sicher aufbewahrt werden kann. Das Ziel sollte außerdem die angeforderte Befehlsklasse nennen. `restart-worker` erklärt mehr als `ssh succeeded`, verrät aber weniger als ein vollständiger Shell-Befehl mit Kundenpfaden und Umgebungsvariablen.

Bereinige Daten vor der Speicherung, nicht erst, nachdem eine prüfende Person das Log geöffnet hat. URL-Abfrageparameter, Request-Header, Shell-Argumente und JSON-Bodies enthalten regelmäßig Tokens. Eine Logging-Bibliothek, die sie «zu Debugging-Zwecken» mitschneidet, wird früher oder später eine Vorfallsdatei voller Zugangsdaten erzeugen.

Diese Struktur hält Zielfakten durchsuchbar, ohne den gesamten Request-Inhalt zu kopieren:

```json
{
  "target": {
    "kind": "http",
    "system_id": "release-service-prod",
    "endpoint": {
      "scheme": "https",
      "host": "api.example.internal",
      "port": 443,
      "method": "POST",
      "route_template": "/v1/releases/{release_id}/promote"
    }
  },
  "operation": {
    "name": "promote_release",
    "request_fingerprint": "sha256:8e8c...",
    "request_bytes": 286,
    "redacted_parameters": {
      "environment": "production",
      "release_id": "rel_7b2"
    }
  }
}
```

Der Fingerabdruck ist nur hilfreich, wenn du eine Kanonisierung definierst. Sortiere Objektfelder, entferne Felder, die deine Bereinigungsregeln ausschließen, normalisiere die Textkodierung und hashe anschließend die resultierenden Bytes. Erfasse die Version der Kanonisierung. Sonst können zwei gleichwertige Anfragen völlig unterschiedliche Hashwerte erzeugen, und eine spätere Änderung des Schemas kann alte Einträge verdächtig wirken lassen.

Ein Hash kann gespeicherte Nachweise nicht ersetzen, wenn eine Aufsichtsbehörde, ein Vertrag oder ein Untersuchungsprozess den ursprünglichen Request verlangt. Verschlüssele den Nachweis in diesem Fall getrennt, beschränke den Zugriff und speichere seinen Inhalts-Hash im Aktions-Eintrag. Lege den ursprünglichen Body nicht allein deshalb in das gewöhnliche durchsuchbare Journal, weil Speicher günstig ist.

## Angeforderte Operation und beobachtetes Ergebnis brauchen unterschiedliche Felder

Eine Agentenanfrage drückt eine Absicht aus. Das Ergebnis beschreibt, was das Gateway nach dem Ausführungsversuch beobachtet hat. Führe beides nicht zu einem vagen Status wie `completed` zusammen.

Bei einem Netzwerkaufruf solltest du die Transportphase erfassen, in der die Ausführung beendet wurde, sofern vorhanden den Protokollstatus, eine begrenzte Antwortzusammenfassung und die benötigte Zeit. Bei SSH gehören Verbindungsergebnis, Authentifizierungsergebnis, Exit-Code des entfernten Prozesses sowie begrenzte Zusammenfassungen von Standardausgabe und Standardfehler dazu. Ein Ziel kann eine Anfrage annehmen und später einen asynchronen Auftrag ausführen. Eine Antwort mit Status 202 bedeutet daher nicht, dass die externe Änderung erfolgt ist.

Verwende Ergebniswerte, die angeben, wo der Fehler auftrat. Zum Beispiel:

- `denied_vault_locked` bedeutet, dass die lokale Geheimnisgrenze den Aufruf abgelehnt hat.
- `denied_approval` bedeutet, dass die erforderliche menschliche Entscheidung ihn nicht erlaubt hat.
- `network_error` bedeutet, dass das Gateway keine Verbindung herstellen oder aufrechterhalten konnte.
- `target_rejected` bedeutet, dass der entfernte Dienst die Anfrage abgelehnt hat.
- `target_accepted` bedeutet, dass der entfernte Dienst die Anfrage angenommen hat.

Lege den rohen Transportfehler nicht in das primäre Statusfeld. Speichere daneben einen normalisierten Code wie `dns_lookup_failed` oder `tls_validation_failed` und eine kurze, bereinigte Diagnose. Für Auswertungen brauchen Entwicklerinnen und Entwickler eine Gruppierung; bei einem Vorfall braucht man genug lokalen Kontext, um ein abgelaufenes Zertifikat von einem blockierten Hostnamen zu unterscheiden.

Ein vollständiges Ergebnisobjekt könnte so aussehen:

```json
{
  "result": {
    "outcome": "target_accepted",
    "started_at": "2025-02-18T21:19:42.184Z",
    "finished_at": "2025-02-18T21:19:43.021Z",
    "duration_ms": 837,
    "http_status": 202,
    "response_fingerprint": "sha256:2a64...",
    "response_summary": "promotion job accepted",
    "evidence_ref": null
  }
}
```

Bezeichne `target_accepted` in deinem Schema nicht als Erfolg. Dieses Wort führt später zu Problemen. Wenn der Dienst einen Auftrag annimmt und der Auftrag scheitert, hat das Gateway genau das getan, was es sollte, während die Geschäftsoperation nicht abgeschlossen wurde. Ein separates Ereignis über den Abschluss auf der Gegenseite, verknüpft durch eine Auftrags-ID, kann diese spätere Frage beantworten.

## Genehmigungs-Einträge müssen festhalten, was die Person genehmigt hat

Ein Genehmigungs-Eintrag braucht eine Entscheidung, einen Umfang, ein Subjekt und Zeitstempel. «Benutzer hat Agenten genehmigt» sagt zu wenig. Prüfer müssen sonst raten, ob die Person einen einzelnen API-Aufruf, einen bestimmten Zugangsschlüssel, eine Sitzung oder jede künftige Sitzung eines ähnlich benannten Prozesses autorisiert hat.

Eine Sitzungsfreigabe erlaubt üblicherweise Aufrufe eines bestimmten Prozesses, bis dieser endet oder jemand ihn widerruft. Eine Freigabe pro Aufruf gilt für die einmalige Nutzung eines Zugangsschlüssels oder einer Operation. Erfasse den Umfang direkt, denn beide Kontrollen führen zu sehr unterschiedlichen Risiken.

```json
{
  "approval": {
    "approval_id": "apr_01J8Q1P4Y5D6",
    "decision": "approved",
    "scope": "session",
    "subject_session_id": "ses_01J8Q1F9K9Z3",
    "approved_at": "2025-02-18T21:14:11Z",
    "expires_at": "2025-02-18T22:02:53Z",
    "approver_presence": "local_user_confirmation"
  }
}
```

Behaupte nicht mehr Sicherheit, als deine Oberfläche liefern kann. Wenn die Anwendung ein lokales Bestätigungsereignis empfängt, erfasse genau diese Tatsache. Speichere nicht den Namen, das Konto beim Identitätsanbieter oder die biometrische Methode einer Person, sofern das System diese Verbindung nicht tatsächlich authentifiziert und unter einer dokumentierten Richtlinie aufbewahrt. Erfunden wirkende Identitätsdetails schaffen falsches Vertrauen und zusätzliche Datenschutzpflichten.

Speichere die Genehmigung, die eine Aktion gesteuert hat, als Kennung im Aktions-Eintrag. Erfasse außerdem die Autorisierungsentscheidung, die unmittelbar vor der Ausführung getroffen wurde. Das wirkt erst dann redundant, wenn du einen zeitlichen Grenzfall untersuchst: Eine alte Sitzungsfreigabe kann existieren, während eine zusätzliche Freigabe pro Aufruf die Aktion ablehnt. Der Aktions-Eintrag braucht beide Fakten.

Genehmigungsmüdigkeit ist ein Designfehler und kein Grund, Nachweise für Genehmigungen wegzulassen. Wenn eine Person jede harmlose Anfrage bestätigen muss, wird sie irgendwann bestätigen, ohne zu lesen. Verlange eine Bestätigung pro Aufruf nur bei Zugangsschlüsseln oder Operationen, deren Missbrauch direkte Aufmerksamkeit erfordert, und vermerke im Eintrag, warum das Gateway gefragt hat.

## Ein Widerruf ist ein Ereignis mit einem Stichtag

Ein Widerruf beendet die Berechtigung für die Zukunft. Er löscht keine Sitzung, holt keine bereits gesendete Anfrage zurück und ändert nicht die Genehmigungsentscheidung, die fünf Minuten zuvor galt.

Erfasse das Ziel des Widerrufs, die Art der auslösenden Person oder Komponente, den beobachteten Zeitpunkt und das Ergebnis der Durchsetzung. Wenn das Gateway eine aktive Sitzung beenden oder blockieren kann, speichere, ob es das getan hat. Wenn eine Aktion die Netzwerkgrenze bereits überschritten hat, muss klar stehen, dass der Widerruf sie nicht zurückholen kann. Während der Reaktion brauchen Entwickler diese unangenehme Tatsache, kein beruhigendes, aber falsches Label wie `revoked`.

Betrachte diese Fehlerfolge:

1. Ein Agentenprozess erhält um 09:00 Uhr eine Sitzungsfreigabe und sendet um 09:17 Uhr eine Änderung in der Produktionsumgebung.
2. Die zuständige Person bemerkt ein unerwartetes Ziel und widerruft die Sitzung um 09:18:04 Uhr.
3. Das Ziel antwortet um 09:18:07 Uhr auf die Anfrage von 09:17 Uhr, weil es die Arbeit bereits eingeplant hatte.
4. Der Agent versucht um 09:18:09 Uhr einen weiteren Aufruf, den das Gateway ablehnt.

Ein gutes Journal bewahrt alle vier Ereignisse. Die Aktion um 09:17 Uhr war zu Beginn autorisiert. Die Antwort nach dem Widerruf gehört zu dieser früheren Aktion. Der abgelehnte Versuch zeigt, dass der Widerruf für spätere Arbeit wirksam war. Wenn du alle früheren Aktionen mit «widerrufen» versiehst, geht die Reihenfolge verloren, die das tatsächliche Risiko erklärt.

Verwende zusätzlich zur Wanduhrzeit eine monotone Sequenznummer. Uhren können voneinander abweichen, Benutzer können die lokale Zeit ändern und Ereignisse können denselben Zeitstempel haben. Eine Sequenznummer zeigt die Reihenfolge, in der das Journal Einträge akzeptiert hat. Wenn du über mehrere Hosts arbeitest, bewahre die lokale Sequenz jedes Hosts auf und verwende Korrelationskennungen, statt so zu tun, als würden Wanduhrzeiten eine perfekte globale Reihenfolge bilden.

## Manipulationsnachweise brauchen ein unveränderliches Anhängen

Eine Hash-Kette erschwert unbemerkte Änderungen, indem jeder neue Eintrag den Hash des vorherigen Eintrags enthält. Sie macht eine gewöhnliche Logdatei aber nicht zum Beweis, dass jedes erwartete Ereignis existiert. Wer den Schreiber und den gespeicherten Kettenkopf kontrolliert, kann ein Suffix löschen, eine neue Kette beginnen oder verhindern, dass Einträge dauerhaft gespeichert werden.

Diese Einschränkung macht Hash-Ketten nicht nutzlos. Sie beantworten eine engere, aber nützliche Frage: Wurden diese Einträge verändert, aus der Mitte entfernt oder nach dem Schreiben in einer anderen Reihenfolge empfangen? Halte die Kettenfelder ausdrücklich fest.

```json
{
  "journal": {
    "sequence": 1842,
    "recorded_at": "2025-02-18T21:19:43.024Z",
    "previous_hash": "sha256:68b1...",
    "record_hash": "sha256:93f4...",
    "hash_format": "canonical-json-v1"
  }
}
```

Hash den vollständigen kanonischen Eintrag und schließe nur `record_hash` selbst aus. Hash nicht die formatierte Darstellung, deren Leerzeichen, Feldreihenfolge oder Zeitformat sich zwischen Versionen ändern können. Versioniere das kanonische Format und bewahre für jedes ausgegebene Format den passenden Prüfer auf.

NIST Special Publication 800-92, Guide to Computer Security Log Management, behandelt Erzeugung, Speicherung, Analyse und Aufbewahrung von Logs als getrennte Verantwortlichkeiten. Diese Unterscheidung räumt mit einem häufigen Fehler auf: Teams fügen Einträgen einen Hash hinzu und betrachten die Aufgabe als erledigt. Du brauchst weiterhin dauerhafte Speicherung, Zugriffskontrollen für den Schreiber, eine Aufbewahrungsentscheidung, regelmäßige Prüfungen und ein Verfahren für den Fall, dass eine Prüfung fehlschlägt.

Sallyport erzeugt seine Sitzungs- und Aktivitätsjournale aus einem schreibgeschützten, verschlüsselten und hashverketteten Audit-Log. Der Befehl `sp audit verify` prüft die Kette offline anhand von Chiffretext, ohne einen Tresorschlüssel zu benötigen. So kann die Integrität des Journals geprüft werden, ohne dass die prüfende Person Zugangsdaten verwenden kann.

## Eine Korrelationskennung macht den Eintrag unter Druck nutzbar

Gib jeder angeforderten Aktion eine Aktions-ID, bevor das Gateway die Autorisierung prüft. Führe sie durch die lokale Entscheidung, den Netzwerkversuch, die Verarbeitung der Antwort und jeden späteren Rückruf über den Abschluss im entfernten System. Wenn eine Entwicklerin oder ein Entwickler eine umstrittene Bereitstellung untersucht, sollte eine Suche nach einer Kennung die vollständige Zeitleiste liefern.

Verwende für unterschiedliche Beziehungen eigene Kennungen. Die Aktion verweist auf Sitzung, Genehmigung, Zugangsschlüsselreferenz, Ziel und Journal-Sequenz. Eine Aufgaben- oder Gesprächsreferenz kann von der Aktion zurück zum Agentenkontext führen, doch das Gesprächsprotokoll darf nicht die Quelle der Wahrheit sein. Prompttext kann sich ändern, private Inhalte enthalten und die spätere Anfrage oft nicht genau beschreiben.

Ein kompaktes vollständiges Objekt kann so aussehen:

```json
{
  "schema_version": "1.0",
  "action_id": "act_01J8Q2ABR8M7",
  "event_type": "action.completed",
  "actor": {"session_id": "ses_01J8Q1F9K9Z3", "pid": 48217},
  "target": {"kind": "http", "system_id": "release-service-prod"},
  "operation": {"name": "promote_release", "request_fingerprint": "sha256:8e8c..."},
  "authorization": {"vault": "unlocked", "approval_id": "apr_01J8Q1P4Y5D6", "decision": "approved"},
  "result": {"outcome": "target_accepted", "http_status": 202},
  "journal": {"sequence": 1842, "previous_hash": "sha256:68b1..."}
}
```

Verwende die Korrelationskennung nicht als Autorisierungstoken. Erzeuge sie unabhängig, mache sie für externe Beobachter unvorhersehbar und akzeptiere niemals den Besitz der Kennung als Berechtigung, eine Aktion zu lesen oder einen Folgeaufruf auszulösen.

## Aufbewahrung muss Nachweise bewahren, ohne einen zweiten Geheimnisspeicher zu schaffen

Audit-Daten sammeln private Informationen, selbst wenn du sie konsequent bereinigst. Zielnamen können Kunden verraten, Routenparameter interne Projekte offenlegen, Antwortzusammenfassungen den Kontostatus sichtbar machen und Prozesspfade Gewohnheiten von Entwicklern offenlegen. Entscheide, wer Einträge durchsuchen und exportieren darf und wie lange die einzelnen Kategorien verfügbar bleiben.

Halte durchsuchbare Aktionsmetadaten von verschlüsselten Request- oder Response-Nachweisen getrennt. Verkürze die Aufbewahrung detaillierter Nachweise, sobald der Untersuchungsbedarf endet. Bewahre Hashes und Verknüpfungsdaten lange genug auf, um zu belegen, dass die verbleibenden Zusammenfassungen weiterhin zu dem passen, was das System aufgezeichnet hat.

Prüfe das Modell mit einer echten Frage aus einer Vorfallbesprechung: «Welcher signierte Prozess hat den Zugriff auf dieses Produktionsziel angefordert, unter welcher Genehmigung und wurde nach dem Widerruf noch ein Versuch unternommen?» Wenn eine Abfrage eine Person dazu zwingt, unstrukturierte Nachrichten zu verknüpfen, Client-Debug-Logs zu untersuchen oder den Agenten zu fragen, woran er sich erinnert, ist das Modell unvollständig.

Sallyports feste Entscheidungskette schafft klare Grenzen für dieses Modell: Tresorsperre, Sitzungsautorisierung und Genehmigung des Zugangsschlüssels pro Aufruf erzeugen jeweils einen eigenen Berechtigungsfakt. Halte diese Fakten auch in deinem Audit-Design getrennt. Der Eintrag sollte zeigen, an welcher Stelle die Berechtigung endete, statt jede Ablehnung unter einem allgemeinen Fehlercode zu verstecken.

Baue das Schema, bevor Agenten weitreichende Zugangsschlüssel erhalten. Wenn du Akteuridentität, Genehmigungsumfang und Reihenfolge der Widerrufe erst nach einem Vorfall ergänzst, musst du Berechtigungen aus Lücken rekonstruieren. Das ist langsam und endet meistens damit, dass jemand «wir glauben» sagt, obwohl der Eintrag genau hätte zeigen sollen, was passiert ist.
