Denkmaschinen · Teil 2 · Sicherheit · 9. Juli 2026

Der Hauptverdächtige ist der eigene Agent

Teil 2 einer Artikelserie über zwei eigene Agenten-Projekte: wintermute (produktiver persönlicher KI-Agent) und agenticframework (produktiv eingesetztes Meta-Framework für autonome Entwicklungs-Pipelines).

Die unbequeme Grundannahme

Im letzten Teil ging es um Souveränität — wer entscheidet, was ein Agent sehen und nutzen darf. Diesmal geht es um die Durchsetzung: Wie stellt man sicher, dass diese Entscheidungen im laufenden Betrieb halten? Und die Antwort beginnt mit einer Annahme, die sich zunächst falsch anfühlt: Der eigene Agent wird behandelt, als wäre er bereits kompromittiert.

Das ist keine Paranoia, sondern Arithmetik. wintermutes docs/SECURITY.md begründet es nüchtern: Der Agent verarbeitet LLM-gesteuert fremde Eingaben — Telegram-Nachrichten, per web_fetch geladene Webseiten. Prompt-Injection ist damit realistisch, nicht hypothetisch. Eine präparierte Webseite, eine manipulierte Nachricht, und die Tool-Entscheidungen des Agenten gehören potenziell jemand anderem. Die dokumentierte Konsequenz ist eine Liste der Dinge, denen das System ausdrücklich nicht vertraut — und ganz oben steht:

"The agent's tool-dispatch decisions. Containers are sandboxed regardless of what the agent decides to call."

Man liest das zweimal. Das System misstraut den Entscheidungen seines eigenen Agenten — nicht weil der Code schlecht wäre, sondern weil ein LLM, das fremde Eingaben liest, per Definition beeinflussbar ist. Sicherheit heißt dann nicht "der Agent trifft keine schlechten Entscheidungen", sondern "schlechte Entscheidungen können keinen Schaden anrichten, der über den Container hinausgeht".

Schicht 1: Privilegien, die es nicht gibt, kann niemand missbrauchen

Die konsequenteste Form dieser Haltung ist der Update-Mechanismus (Lesson #9, in Teil 0 kurz erwähnt — hier die Architektur dahinter). Ursprünglich hätte ein Agent, der sich selbst deployen will, Docker-Socket und SSH-Schlüssel gebraucht. Stattdessen läuft auf dem Host ein separater, winziger Dienst als systemd-Unit, und der Agent ist nur noch HTTP-Client. Das Entscheidende ist, wie wenig dieser Dienst kann: vier Endpunkte (/healthz, /version, /update, /update/dry-run), und /update tut genau eine Sache — git fetch + reset --hard origin/main, dann die Container neu bauen. Branch und Services sind per Env-Var allow-listed, beim Start des Dienstes fixiert.

Der Agent kann also nicht "irgendetwas deployen". Er kann exakt eine Aktion auslösen: den aktuellen Stand des Haupt-Branches ausrollen. Kein Parameter, mit dem sich ein anderer Branch, ein anderes Repo oder ein anderer Befehl unterschieben ließe. Ein per Prompt-Injection gekaperter Agent kann damit schlimmstenfalls... ein reguläres Deployment auslösen. Das ist die eigentliche Kunst bei Agenten-Sicherheit: nicht Fähigkeiten absichern, sondern Fähigkeiten so eng zuschneiden, dass ihr Missbrauch uninteressant wird.

Dasselbe Denken bei den Secrets — bis in die Unix-Dateirechte:

# Read-only mount of the updater shared-secret file. Host file is
# 0640 root:wintermute-secrets (gid set by WINTERMUTE_SECRETS_GID,
# default 2000). The container's appuser is a member of the same
# numeric gid, so the read works without making the file
# world-readable.
- ${UPDATER_TOKEN_FILE:-/etc/wintermute/updater.token}:/run/secrets/updater_token:ro

Kein chmod 644, "weil es sonst nicht läuft" — sondern eine dedizierte Gruppe, deren numerische GID in den Container verlängert wird. Unspektakulär, aber genau die Stelle, an der in der Praxis geschludert wird.

Schicht 2: Containment im Loop selbst

Vertrauensgrenzen gibt es auch innerhalb des Agenten-Loops. Zwei Mechanismen begrenzen, was ein außer Kontrolle geratener (oder manipulierter) Agent pro Turn anrichten kann: ein Iterations-Cap (standardmäßig 25 Tool-Runden pro Turn) und ein Circuit-Breaker gegen identische Wiederholungen. Ruft das Modell dasselbe Tool mit exakt denselben Argumenten zum wiederholten Mal auf, wird der Aufruf nicht ausgeführt, sondern durch ein synthetisches Tool-Ergebnis ersetzt:

return {
    "error": (
        f"FRAMEWORK CIRCUIT-BREAKER: you've already issued "
        f"{summary} {prior_count} time(s) this turn with these "
        "exact arguments. Each prior execution returned to you "
        "as a tool result. If the previous result(s) contained "
        "an `error` or `error_code` field, that is the actionable "
        "signal -- read it and either change the arguments or "
        "stop and report to the user."
    ),
    "error_code": "framework_duplicate_tool_call",
    ...
}

Bemerkenswert ist die Adressierung: Diese Fehlermeldung ist nicht für Menschen geschrieben, sondern für das Modell — sie erklärt ihm, was es falsch macht und was der nächste sinnvolle Schritt wäre. Fehlerbehandlung in Agenten-Systemen hat zwei Zielgruppen, und die zweite sitzt im Loop.

Und für Webzugriffe gibt es einen SSRF-Guard, der die Klassiker abdeckt: web_fetch löst den Zielhost auf und verweigert Loopback-, Link-Local-, private und reservierte Adressen — inklusive der Cloud-Metadata-Adresse 169.254.169.254 und, hübsches Detail, der eigenen internen Dienste (Qdrant, der Host-Updater). Redirects werden manuell verfolgt, damit jeder einzelne Hop erneut geprüft wird. Eine bösartige Webseite kann den Agenten also nicht per Redirect auf seine eigene Infrastruktur lenken.

Schicht 3: Jede Schicht rechnet mit dem Versagen der vorherigen

Das Muster hinter alldem ist Defense in Depth, und wintermute buchstabiert es ungewöhnlich konsequent aus. Beispiel Workspace-Zugriff (Issue #50): Es existiert erstens gar kein Schreib-Tool für lokale Repos — und zweitens ist das Volume trotzdem :ro gemountet. Falls also je ein Write- oder Exec-Tool dazukommt und jemand die Guard-Logik vergisst, schlägt immer noch das Dateisystem fehl.

Beispiel Self-Repo (Ebene 3 der Policy): Selbst wenn alle lokalen Guards versagen, verlangt GitHub-Branch-Protection auf main Pull Requests plus Review und verbietet Force-Pushes —

"Even if everything else were to fail, GitHub itself returns 403 for direct push to main. This is the ultimate backstop and survives any local code bug."

Die letzte Verteidigungslinie liegt bewusst außerhalb der eigenen Codebasis. Ein Bug in wintermute kann wintermutes Regeln brechen — aber nicht GitHubs.

Dieselbe Idee als Organisationsprinzip: das Veto, das niemand aufheben kann

In agenticframework wird aus der Container-Grenze eine Prozessgrenze. In Phase 5 der Pipeline laufen zwei Prüf-Agenten parallel (asyncio.gather): Argus (Stil, Tests) und Molly (Security/SAST). Beide müssen GO geben. Der Unterschied zwischen ihnen ist die interessanteste Governance-Entscheidung des ganzen Frameworks: Argus' Veto kann ein Mensch per Waiver überstimmen — Stil ist verhandelbar. Mollys Veto nicht:

if result.artifact_kind is ArtifactKind.SAST_RESULT and verdict == "no_go":
    return self._decision(
        result.phase,
        artifact_refs,
        GateDecisionStatus.NO_GO,
        "SAST security veto blocks advancement.",
        error_code=ErrorCode.SECURITY_VETO,
        human_override_ref=human_override_ref,
    )

Man beachte: human_override_ref wird entgegengenommen und dokumentiert — aber der Status bleibt NO_GO. Die Spezifikation formuliert es trocken: "human override is recorded but ignored". Wer versucht, ein Security-Veto zu überstimmen, erzeugt keinen Deploy, sondern einen Audit-Eintrag über den Versuch. Das ist gelebte Erfahrung aus jeder gewachsenen Organisation: Für Stilfragen braucht man Pragmatismus, für Sicherheitsfragen braucht man eine Instanz, die auch unter Termindruck nicht weich wird — notfalls, indem man ihr die Fähigkeit zum Weichwerden technisch entfernt.

Dazu passt die Credential-Politik beim Agenten-Start: Ein Agent deklariert, welche Connectors er braucht, und bekommt beim Spawn ein kurzlebiges Token, das auf exakt diese Connectors beschränkt ist. Ein Root-Credential sieht er nie.

Der ehrliche Teil: Sicherheit heißt begrenzter Schaden, nicht kein Schaden

Was mir an wintermutes Security-Dokumentation am besten gefällt, ist ausgerechnet der Abschnitt über das eigene Restrisiko. Der HMAC-Token des Updaters könnte kompromittiert werden — und statt das wegzulächeln, steht da die Blast-Radius-Rechnung: Ein Angreifer im Host-Netz könnte damit /update aufrufen, was auf das Deployen von origin/main begrenzt ist. Mitigation: Dateirechte, Firewall auf dem Port, dokumentierter Rotations-Befehl. Das ist der Reifegrad, an dem man Sicherheitsarchitekturen messen sollte: nicht "bei uns kann nichts passieren", sondern "hier ist, was passieren kann, so weit haben wir es eingegrenzt, und so rotiert man den Schlüssel".

Sicherheit bei autonomen Agenten ist kein Feature, das man aktiviert. Es ist die Summe vieler kleiner, gegenseitig redundanter Entscheidungen — und die Bereitschaft, dem charmantesten Mitarbeiter im System grundsätzlich nicht zu glauben.

Ausblick

Der nächste Teil führt das konsequent weiter, von der Infrastruktur ins Verhalten: Was tut man, wenn der Agent nicht böswillig manipuliert wird, sondern schlicht Dinge erfindet — selbstbewusst, plausibel und mit hübschen Commit-IDs, die es nie gab? Es wird um Halluzinationen gehen, um einen Validator, der sie fängt, und um die Klasse, die durch jedes Raster fällt.


Quellen: wintermute/docs/SECURITY.md (Threat Model, SSRF-Guard, Token-Blast-Radius), wintermute/LESSONS.md (Lesson #9), wintermute/updater/src/wintermute_updater/app.py (Endpunkte, Allow-Lists), wintermute/systemd/wintermute-updater.service, wintermute/docker-compose.yml (Secret-Mount, group_add, Workspace :ro), wintermute/docs/WORKSPACE-TOOLS.md (Issue #50), wintermute/docs/GITHUB-TOOLS.md (Branch-Protection als Backstop), wintermute/agent/src/agent/core.py (Iterations-Cap, Duplicate-Call-Circuit-Breaker); agenticframework/FRAMEWORK.md (Gate-Regeln vertical-slice-gates-v1, Credential Scoping), agenticframework/orchestrator/src/agentic_orchestrator/gates/policy.py (SAST-Veto), .../kernel/executor.py (paralleles Compound-Gate Phase 5), .../kernel/models.py (HumanDecision im Manifest).