Stufe 3: reversible Pseudonymisierung im Router — Eigenbau nach Cordon-Vorbild, Korpus zuerst #1

Open
opened 2026-09-12 06:49:00 +02:00 by elton · 0 comments
Owner

Bezug: Vorgang und Dreistufen-Zuschnitt in live/live#1751, Plattform-Sicht in spine/core#21. Dort ist Stufe 3 („reversible Pseudonymisierung nach Cordon-Vorbild") als eigenes Ticket vorgesehen, sobald Stufe 1 und 2 stehen. Dieses Ticket ist jener Vorschlag — konkretisiert durch die Feldsicht vom 12.09.2026 und durch das Lesen des aktuellen Codes. Stufe 1 (Secret-Scan fail-closed) und Stufe 2 (harte Lokal-nur-ACL) bleiben in #1751 und sind nicht Teil davon.

Vorbild: Cordon — Node/TS-Drop-in-Proxy, deterministische Erkennung per Regex plus Prüfsummen, Modi reversible/strip/off, fail-closed.

Entscheidung: Eigenbau, kein Fremd-Dienst in der Kette

Der Router ist der einzige LLM-Endpunkt der ganzen Flotte. Dass er am 09.09.2026 für alle vier Agenten und den Mac ausfiel, ist genau das Argument gegen einen weiteren synchronen Hop: jede zusätzliche Komponente erbt diese Kritikalität, ohne sie zu verdienen.

Die Feldsicht vom 12.09.2026 (Lizenzen und Pflegezustand am 12.09. gegen GitHub gemessen) ergibt nichts, was wir als Fremd-Dienst einsetzen wollten:

Lösung Lizenz/Status Warum nicht
Portkey MIT-Gateway, 13k★ PII-Redaction ist Enterprise-/Hosted-Feature, nicht im offenen Teil
LLM Guard (Protect AI) war MIT Repo archiviert (Protect AI ging an Palo Alto Networks) — keine gepflegte Basis
PrivAiTe BSD-3, 38★ Konzept passt, aber 38 Sterne, ein Jahr alt: würde Mitbesitzer unserer kritischsten Kette
philter-ai-proxy Apache-2.0, 4★ nur Schwarzung, kein Gateway; Philter selbst älter und gepflegt
whitestag pii-proxy Apache-2.0, 2★ DE-Felder (IBAN, BIC, USt-ID) und AES-GCM-Mapping gut gemacht — als Code-Vorbild, nicht als Abhängigkeit
Sphragis, Zaps, MaskChain, Occludra, AgentVeil, token-proxy, LLM-Shield Apache-2.0/MIT, 0–84★ dasselbe Bild: Konzept ja, Reifepfad nein
Persona (mind-verse), Lucairn, Xybern, NOPII, Requesty proprietär / SaaS weiterer Auftragsverarbeiter im Prompt-Pfad; bei Zustandslosigkeit wandert zudem die Zuordnungstabelle durchs Netz

Cloud-DLP scheidet aus, weil es die Rohdaten erst recht nach außen schickt. Was offen und gereift ist, ist die Erkennungsengine (Presidio, MIT, rund 11k★) — die kann als Sidecar hinter einer eigenen Schnittstelle laufen, die Architektur bleibt Eigenbau.

Der Bauplan, den #1751 noch nicht hat: dritte Schiene

#1751 Befund 1 hält richtig fest, dass Regex Schuldner-Namen und Anschriften im Freitext nicht zuverlässig fängt, und beantwortet das mit NER. Für unsere Daten ist NER aber der unbequemere Weg: probabilistisch, in Deutsch mittelmäßig, mit einem Modell, das jemand pflegen muss. Der nähere Weg ergibt sich daraus, dass das Risiko bei uns schon strukturiert vorliegt.

  • Schiene 1 — deterministisch. Regex plus Prüfsummen wie Cordon: Mail, Telefon DE, IBAN (ISO 7064 mod-97), Karten (Luhn), Aktenzeichen, Secrets.
  • Schiene 2 — Wörterbuch aus dem Mandantenbestand. Für Klara und Inka sind Name, Anschrift, Telefon und Aktenzeichen Spalten im Mandantenregister. Ein Aho-Corasick-Abgleich gegen den echten Bestand des betreffenden Mandanten trifft genau die Freitext-Namen, die kein Regex und keine deutsche NER zuverlässig fängt — deterministisch und mit hohem Recall. Ein Treffer ist beweisbar, kein Score.
  • Schiene 3 — NER, optional und später. Presidio-Sidecar (Analyzer mit GLiNER/spaCy, Anonymizer mit reversiblem encrypt-Operator), falls Schiene 2 nach dem Korpus-Test Lücken lässt. Nicht vor dem Korpus entscheiden.

Einbauorte (am Stand vom 12.09. gelesen)

Alle nötigen Punkte existieren bereits, das Ticket ist additiv:

  • src/server.ts:196 jsonBody() — die eine Stelle, an der der Upstream-Body entsteht. Hier sitzt der Rewrite.
  • src/server.ts:444 — der SSE-TransformStream, der heute den usage-Chunk zeilenweise mitliest. Hier sitzt die Rückführung im Strom; die Zeilenpuffer-Logik ist vorhanden, neu ist der Grenz-Puffer gegen zerfallende Platzhalter ([PERSON_ / 7]).
  • Der JSON-Antwortzweig direkt daneben.
  • Policy an key-acls.json — die Identität des Aufrufers ist je Anfrage schon bekannt.

Zwingend: rekursiv durch das ganze JSON, nicht nur messages[].content. Die Tool-Call-Argumente sind die Lücke, über die alles andere wertlos wird. Der PrivAiTe-Benchmark (deren eigener, mit Vorsicht zu lesen) misst für das LiteLLM-Presidio-Guardrail und für LLM Guard 100 % unveränderte PII in tool_calls[].function.arguments — erkannt wird sie in denselben Systemen im Klartext. Und umgekehrt: ein maskierter Wert, der über einen Tool-Aufruf zurück in unsere Datenhaltung läuft, muss am richtigen Übergang aufgelöst werden, sonst landet PERSON_7 in einem Vorgang (#1751 Befund 3).

Aktivierung pro Key, nie pauschal

Falsch wäre, den Router als Ganzen zu scrubben: Über llm.lan laufen auch opencode und Claude Code. Hostnamen, IP-Adressen, Pfadnamen und Bezeichner pseudonymisiert sind für Code-Agenten keine Sicherheit, sondern Zerstörung. Für diese Keys ist die ehrliche Regel „keine Fachdaten im Prompt", nicht Maskierung.

An ist es bei den Pfaden mit echtem Kundenbezug: Klara (Mailtexte) und Inka (Telefontranskripte). Modus je Key: off / strip / reversible.

Erster Schritt: das Akzeptanzkorpus

Vor jeder Zeile Erkennungslogik ein Fixture mit erwarteten Funden: reale Mailtexte und Transkript-Ausschnitte, mit fiktiven, aber realistischen Werten — keine produktiven Personen- oder Auftragsdaten ins Repo, Klassifikation beachten. Ebenso Negativproben: Quelltext, Konfigurationsdateien, Hostnamen und Logs, die nicht getroffen werden dürfen. Fehlalarme sind bei Code-Keys der Ausfallgrund.

Der Grund, warum das zuerst kommt: ohne Korpus ist „Test grün" nicht von „es ist immer noch alles durchgerutscht" zu unterscheiden. Die Fehlerverteilung ist asymmetrisch — zu viel maskiert ist lästig, ein durchgelassener Name ist der Vorfall, für den das hier gebaut wird.

Mapping-Behandlung

Sitzungsbezogene Tabelle, TTL, verschlüsselt abgelegt (AES-GCM, wie Cordon und whitestag pii-proxy es vormachen). Sie darf niemals in requests.jsonl oder in die SQLite-Kostenzeile landen — der Log hält heute nur Metadaten (ts, key, ticket, alias, Modell, Upstream, Tokens, Kosten), und das muss so bleiben. Ergänzt wird höchstens scrubbed: true plus Zähler je Kategorie, nie ein Entitätswert.

Zu entscheiden (Andreas, ggf. mit Festus)

  1. Wird die Zuordnung nach der Antwort gelöscht? Dann ist der nach außen gegangene Datensatz anonymisiert statt nur pseudonymisiert — das verändert die Drittlandbetrachtung und die AVV-Kette grundlegend. Löschen heißt: kein Nachschauen mehr, welche Anfrage ein Pseudonym trug.
  2. Obligatorik. Policy am Key (nicht umgehbar) oder zusätzliche Client-Vorgabe pro Anfrage? Ein Header, der das Scrubben ausstellt, ist ein Riegel, den der Aufrufer selbst öffnen kann.
  3. Schiene 3 ja oder nein — erst nach dem Korpus-Test messen, ob Schiene 2 allein genügt.

Umfang

src/redact.ts mit Erkennung, Map je Vorgang und Rückführung: einige hundert Zeilen, ohne Änderung an einem einzigen Client (#1751). Der Löwenanteil liegt nicht in der Erkennung, sondern im Korpus und im SSE-Grenzpuffer.

Hinweis zur Beschriftung

Die Org spine führt keine Labels — type/Anforderung und prio/wichtig waren beim Anlegen gesetzt, sind aber still verloren (dieselbe Erscheinung, weswegen #1122/#1123 damals ohne Label ankamen). Nachzuholen ist das nur über ein Label-Set in der Org, nicht am Ticket.

**Bezug:** Vorgang und Dreistufen-Zuschnitt in [live/live#1751](https://git.42i.org/live/live/issues/1751), Plattform-Sicht in [spine/core#21](https://git.42i.org/spine/core/issues/21). Dort ist Stufe 3 („reversible Pseudonymisierung nach Cordon-Vorbild") als eigenes Ticket vorgesehen, sobald Stufe 1 und 2 stehen. Dieses Ticket ist jener Vorschlag — konkretisiert durch die Feldsicht vom 12.09.2026 und durch das Lesen des aktuellen Codes. Stufe 1 (Secret-Scan fail-closed) und Stufe 2 (harte Lokal-nur-ACL) bleiben in #1751 und sind **nicht** Teil davon. Vorbild: [Cordon](https://github.com/askalf/cordon) — Node/TS-Drop-in-Proxy, deterministische Erkennung per Regex plus Prüfsummen, Modi `reversible`/`strip`/`off`, fail-closed. ## Entscheidung: Eigenbau, kein Fremd-Dienst in der Kette Der Router ist der einzige LLM-Endpunkt der ganzen Flotte. Dass er am 09.09.2026 für alle vier Agenten und den Mac ausfiel, ist genau das Argument gegen einen weiteren synchronen Hop: jede zusätzliche Komponente erbt diese Kritikalität, ohne sie zu verdienen. Die Feldsicht vom 12.09.2026 (Lizenzen und Pflegezustand am 12.09. gegen GitHub gemessen) ergibt nichts, was wir als Fremd-Dienst einsetzen wollten: | Lösung | Lizenz/Status | Warum nicht | |---|---|---| | Portkey | MIT-Gateway, 13k★ | PII-Redaction ist Enterprise-/Hosted-Feature, nicht im offenen Teil | | LLM Guard (Protect AI) | war MIT | **Repo archiviert** (Protect AI ging an Palo Alto Networks) — keine gepflegte Basis | | PrivAiTe | BSD-3, 38★ | Konzept passt, aber 38 Sterne, ein Jahr alt: würde Mitbesitzer unserer kritischsten Kette | | philter-ai-proxy | Apache-2.0, 4★ | nur Schwarzung, kein Gateway; Philter selbst älter und gepflegt | | whitestag pii-proxy | Apache-2.0, 2★ | DE-Felder (IBAN, BIC, USt-ID) und AES-GCM-Mapping gut gemacht — als Code-Vorbild, nicht als Abhängigkeit | | Sphragis, Zaps, MaskChain, Occludra, AgentVeil, token-proxy, LLM-Shield | Apache-2.0/MIT, 0–84★ | dasselbe Bild: Konzept ja, Reifepfad nein | | Persona (mind-verse), Lucairn, Xybern, NOPII, Requesty | proprietär / SaaS | weiterer Auftragsverarbeiter im Prompt-Pfad; bei Zustandslosigkeit wandert zudem die Zuordnungstabelle durchs Netz | Cloud-DLP scheidet aus, weil es die Rohdaten erst recht nach außen schickt. Was offen und gereift ist, ist die **Erkennungsengine** (Presidio, MIT, rund 11k★) — die kann als Sidecar hinter einer eigenen Schnittstelle laufen, die Architektur bleibt Eigenbau. ## Der Bauplan, den #1751 noch nicht hat: dritte Schiene #1751 Befund 1 hält richtig fest, dass Regex Schuldner-Namen und Anschriften im Freitext nicht zuverlässig fängt, und beantwortet das mit NER. Für unsere Daten ist NER aber der unbequemere Weg: probabilistisch, in Deutsch mittelmäßig, mit einem Modell, das jemand pflegen muss. Der nähere Weg ergibt sich daraus, dass das Risiko bei uns **schon strukturiert vorliegt**. - **Schiene 1 — deterministisch.** Regex plus Prüfsummen wie Cordon: Mail, Telefon DE, IBAN (ISO 7064 mod-97), Karten (Luhn), Aktenzeichen, Secrets. - **Schiene 2 — Wörterbuch aus dem Mandantenbestand.** Für Klara und Inka sind Name, Anschrift, Telefon und Aktenzeichen Spalten im Mandantenregister. Ein Aho-Corasick-Abgleich gegen den echten Bestand des betreffenden Mandanten trifft genau die Freitext-Namen, die kein Regex und keine deutsche NER zuverlässig fängt — **deterministisch und mit hohem Recall**. Ein Treffer ist beweisbar, kein Score. - **Schiene 3 — NER, optional und später.** Presidio-Sidecar (Analyzer mit GLiNER/spaCy, Anonymizer mit reversiblem `encrypt`-Operator), falls Schiene 2 nach dem Korpus-Test Lücken lässt. Nicht vor dem Korpus entscheiden. ## Einbauorte (am Stand vom 12.09. gelesen) Alle nötigen Punkte existieren bereits, das Ticket ist additiv: - `src/server.ts:196` `jsonBody()` — die eine Stelle, an der der Upstream-Body entsteht. Hier sitzt der Rewrite. - `src/server.ts:444` — der SSE-`TransformStream`, der heute den `usage`-Chunk zeilenweise mitliest. Hier sitzt die Rückführung im Strom; die Zeilenpuffer-Logik ist vorhanden, neu ist der Grenz-Puffer gegen zerfallende Platzhalter (`[PERSON_` / `7]`). - Der JSON-Antwortzweig direkt daneben. - Policy an `key-acls.json` — die Identität des Aufrufers ist je Anfrage schon bekannt. **Zwingend: rekursiv durch das ganze JSON, nicht nur `messages[].content`.** Die Tool-Call-Argumente sind die Lücke, über die alles andere wertlos wird. Der PrivAiTe-Benchmark (deren eigener, mit Vorsicht zu lesen) misst für das LiteLLM-Presidio-Guardrail und für LLM Guard **100 % unveränderte PII in `tool_calls[].function.arguments`** — erkannt wird sie in denselben Systemen im Klartext. Und umgekehrt: ein maskierter Wert, der über einen Tool-Aufruf zurück in unsere Datenhaltung läuft, muss am richtigen Übergang aufgelöst werden, sonst landet `PERSON_7` in einem Vorgang (#1751 Befund 3). ## Aktivierung pro Key, nie pauschal Falsch wäre, den Router als Ganzen zu scrubben: Über `llm.lan` laufen auch opencode und Claude Code. Hostnamen, IP-Adressen, Pfadnamen und Bezeichner pseudonymisiert sind für Code-Agenten keine Sicherheit, sondern Zerstörung. Für diese Keys ist die ehrliche Regel „keine Fachdaten im Prompt", nicht Maskierung. An ist es bei den Pfaden mit echtem Kundenbezug: Klara (Mailtexte) und Inka (Telefontranskripte). Modus je Key: `off` / `strip` / `reversible`. ## Erster Schritt: das Akzeptanzkorpus Vor jeder Zeile Erkennungslogik ein Fixture mit erwarteten Funden: reale Mailtexte und Transkript-Ausschnitte, mit fiktiven, aber realistischen Werten — **keine produktiven Personen- oder Auftragsdaten ins Repo**, Klassifikation beachten. Ebenso Negativproben: Quelltext, Konfigurationsdateien, Hostnamen und Logs, die nicht getroffen werden dürfen. Fehlalarme sind bei Code-Keys der Ausfallgrund. Der Grund, warum das zuerst kommt: ohne Korpus ist „Test grün" nicht von „es ist immer noch alles durchgerutscht" zu unterscheiden. Die Fehlerverteilung ist asymmetrisch — zu viel maskiert ist lästig, ein durchgelassener Name ist der Vorfall, für den das hier gebaut wird. ## Mapping-Behandlung Sitzungsbezogene Tabelle, TTL, verschlüsselt abgelegt (AES-GCM, wie Cordon und whitestag pii-proxy es vormachen). Sie darf **niemals** in `requests.jsonl` oder in die SQLite-Kostenzeile landen — der Log hält heute nur Metadaten (ts, key, ticket, alias, Modell, Upstream, Tokens, Kosten), und das muss so bleiben. Ergänzt wird höchstens `scrubbed: true` plus Zähler je Kategorie, nie ein Entitätswert. ## Zu entscheiden (Andreas, ggf. mit Festus) 1. **Wird die Zuordnung nach der Antwort gelöscht?** Dann ist der nach außen gegangene Datensatz anonymisiert statt nur pseudonymisiert — das verändert die Drittlandbetrachtung und die AVV-Kette grundlegend. Löschen heißt: kein Nachschauen mehr, welche Anfrage ein Pseudonym trug. 2. **Obligatorik.** Policy am Key (nicht umgehbar) oder zusätzliche Client-Vorgabe pro Anfrage? Ein Header, der das Scrubben ausstellt, ist ein Riegel, den der Aufrufer selbst öffnen kann. 3. **Schiene 3 ja oder nein** — erst nach dem Korpus-Test messen, ob Schiene 2 allein genügt. ## Umfang `src/redact.ts` mit Erkennung, Map je Vorgang und Rückführung: einige hundert Zeilen, ohne Änderung an einem einzigen Client (#1751). Der Löwenanteil liegt nicht in der Erkennung, sondern im Korpus und im SSE-Grenzpuffer. ## Hinweis zur Beschriftung Die Org `spine` führt keine Labels — `type/Anforderung` und `prio/wichtig` waren beim Anlegen gesetzt, sind aber still verloren (dieselbe Erscheinung, weswegen #1122/#1123 damals ohne Label ankamen). Nachzuholen ist das nur über ein Label-Set in der Org, nicht am Ticket.
elton added the
prio
wichtig
type
Anforderung
labels 2026-09-12 06:57:21 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: spine/llmrouter#1