Zurück zum Blog
·von Patrick Hofmann

Der Daemon ist kein Backdoor, er ist nur ein weiterer Ape

Ich hatte den WS-Handler des Nest-Daemons gebaut, als müsste er mein IdP-Token benutzen. Der Daemon läuft aber als eigener Service-User ohne Zugriff darauf. Die Lösung war nicht, ihm mein Token zu geben — sondern ihm eine eigene Identität.

OpenApeAI AgentsInfrastructureSecurityBuilding in Public

Der Nest-Daemon ist ein Control-Plane: ein langlebiger Prozess, der pro Maschine läuft, Agenten supervidiert und Spawn-Intents entgegennimmt. Ich habe ihm einen WebSocket-Handler gebaut, der sich beim troop-SP anmeldet, damit der Server Spawn-Befehle pushen kann statt alle fünf Minuten gepollt zu werden. Der Handler rief ensureFreshIdpAuth() auf — hol das frische IdP-Token, schick es über den Socket. In der Plan-Phase sah das sauber aus. Beim ersten echten Start lief es nie.

Der Daemon läuft als _openape_nest, ein versteckter Service-User, HOME=/var/openape/nest. Da liegt kein auth.json. ensureFreshIdpAuth() hatte nichts zu holen, weil dort nie jemand apes login ausgeführt hat. Es gibt keinen Menschen in diesem HOME.

Wie es vorher gedacht war

Die Annahme im Plan: der Daemon ist meine Infrastruktur. Ich starte ihn, also operiert er mit meiner Identität. Das alte Poll-Modell hat genau so funktioniert — nur dass es das Problem umging, ohne dass mir das auffiel. Der 5-Minuten-Sync rief pro Agent apes run --as <agent> auf. Jeder Agent hat eine eigene DDISA-Identity in seinem eigenen HOME. Der setuid-Sprung in den Agent-User war die Auth. Der Daemon selbst brauchte nie ein Token, weil er nie selbst gesprochen hat — er hat immer nur einen Agenten reden lassen.

Der WebSocket-Pfad hat das nicht. Eine persistente Verbindung zum Server gehört dem Daemon, nicht einem Agenten. Der Daemon muss selbst sprechen. Und in dem Moment, in dem er selbst spricht, braucht er eine Identität — und ich hatte im Plan stillschweigend angenommen, das sei meine. WS-Auth ist ein anderes Modell als der per-agent Sync, nicht eine Variante davon. Diesen Schnitt habe ich übersehen.

Warum die naheliegende Lösung falsch ist

Der Reflex ist offensichtlich: apes login als ich, das auth.json nach /var/openape/nest/ kopieren, fertig. Funktioniert auch. Genau deshalb ist es gefährlich.

Der Daemon ist langlebig, läuft als systemnaher Service, supervidiert andere Prozesse. Das ist die Stelle in der Architektur, an der ein Owner-Token am wenigsten verloren gehen darf. Und selbst wenn es nicht leakt: jede Aktion, die der Daemon dann ausführt, trägt act:human mit meinem Subject. Wenn der Daemon einen Agenten spawnt, steht im Audit-Log, ich hätte das getan. Das Log lügt dann nicht aus Böswilligkeit, sondern weil die Identität falsch modelliert ist.

Ein Control-Plane-Daemon, der das Token seines Owners hält, ist per Definition ein Backdoor. Nicht weil jemand ihn dazu macht — sondern weil die Trust-Annahme "dieser privilegierte Prozess handelt als ich" genau das ist, was eine Sicherheits-Architektur nicht tragen sollte.

Der Shift

Der Daemon ist nicht meine Infrastruktur mit meinen Credentials. Er ist selbst ein Ape.

_openape_nest bekommt eine eigene DDISA-Agent-Identity, eigenes Ed25519-Keymaterial, ein eigenes apes login in seinem eigenen HOME. Der WS-Handler von troop akzeptiert diese Identität — act:agent neben act:human, nicht statt:

// troop WS-Handler: beide Akteure sind erstklassig,
// keiner ist ein Sonderfall des anderen
const claim = verifyActClaim(token);
if (claim.act === "human") {
  // Owner verbindet sich aus dem Browser / der CLI
} else if (claim.act === "agent") {
  // der Nest-Daemon verbindet sich mit seiner eigenen DDISA-Identity
} else {
  return reject(socket, "unknown actor");
}

Der Daemon spricht jetzt als er selbst. Spawn-Intents, die über diesen Socket reinkommen, sind nicht meine Aktionen, die der Daemon durchreicht — sie sind Aktionen des Daemons, mit seinem Subject im Audit-Trail.

Bleibt eine Frage: ein Spawn passiert für mich. Der Daemon hat sein eigenes Token, aber meines ist auf der Maschine nicht erreichbar — und soll es auch nicht sein. Wie handelt der Daemon on-behalf-of-human, ohne mein Token zu haben?

Über einen Delegation-Grant. RFC 8693 Token-Exchange ist eigentlich strikt: subject_token ist Pflicht, weil der typische Fall ein confidential Client ist, der beide Tokens hat. Unser Fall ist ein anderer — der Daemon hat sein eigenes Token, meines ist unerreichbar. Pragmatik: subject_token wird optional, wenn eine delegation_grant_id da ist; der IdP leitet den Delegator aus dem Grant ab. Nicht mehr strikt RFC, aber derselbe Effekt — und der HITL-Punkt bleibt da, wo er hingehört: im escapes-Grant-Flow, nicht in einem kopierten Token. Ich approve eine Delegation einmal, der Daemon agiert in ihrem Rahmen, jeder Schritt ist server-seitig auditbar.

Was weggefallen ist

ensureFreshIdpAuth() im Daemon-Pfad. Der ganze "hol Patricks Token, schick es weiter"-Codeweg. Es gibt kein durchgereichtes Owner-Token mehr, also gibt es auch keine Stelle, an der es leaken kann.

Stattdessen: einmal apes login als _openape_nest, der ssh-Keypair wird bei der Migration zum Service-User mitkopiert, damit die IdP-Identität gleich bleibt — kein Re-Enroll, alle Delegations und Grants persistieren. Ein Stolperstein dabei: das 1h-Token-Expiry trifft, und der Auto-Refresh kann am stale key_path im migrierten auth.json scheitern. Lösung: nach der Migration explizit apes login --key triggern. Pattern ist wiederverwendbar für jeden anderen OpenApe-Daemon, der denselben Weg geht.

Der Schnitt

Ich wollte dem Daemon mein Token geben. Es wurde: der Daemon kriegt eine eigene Identität.

Das ist nicht nur ein Auth-Fix. Es ist ein Hinweis, den ich jetzt überall wiedererkenne. Immer wenn eine Komponente "privilegierte Infrastruktur" sein will — die mit den Owner-Credentials läuft, die als der Mensch handelt, die einen Sonderpfad in der Auth braucht — ist das fast immer ein Symptom, nicht ein Designziel. Es heißt: diese Komponente hat noch keine eigene Identität, und ich habe die Lücke mit meiner gestopft.

Gib ihr eine eigene, und die Sonderbehandlung verschwindet. Der Daemon ist dann kein Backdoor mehr, der mein Token hält. Er ist nur ein weiterer Ape — gleiches Protokoll, eigenes Schlüsselmaterial, HITL über denselben Grant-Flow wie alle anderen.


Code: github.com/openape-ai/openape, MIT-lizenziert. Der Nest-Daemon lebt in @openape/nest, der WS-Handler in der troop-SP. Der Delegation-only Token-Exchange-Pfad lebt im IdP-Auth-Code (exchangeWithDelegation in @openape/cli-auth).