Interne Systeme an Claude anbinden: was ich beim Bau eines MCP-Gateways gelernt habe
Ich wollte herausfinden, ob sich unsere internen, nur über VPN erreichbaren Systeme aus Claude heraus nutzbar machen lassen, mit echter Identität, und ohne dass

Ich wollte herausfinden, ob sich unsere internen, nur über VPN erreichbaren Systeme aus Claude heraus nutzbar machen lassen, mit echter Identität, und ohne dass die Verbindung zu einem Kanal für abfließende Daten wird.
Das klingt nach einem Problem. Es sind eigentlich drei, und sie werden gern zu einem verrührt:
Das erste lösen die meisten Beiträge. Das dritte fand ich interessant, und dort ist am Ende die meiste Arbeit gelandet.
Herausgekommen ist ein Proof of Concept: ein kleines Gateway, das ein simuliertes internes Ticketsystem und später eine echte Nextcloud-Instanz an claude.ai anbindet, mit OAuth-Login, benutzerbezogenen Berechtigungen, Antwortlimits und einem Audit-Log. Es wurde nicht gebenchmarkt, nicht lasttestet, es gab kein Security Review und keinen Produktionsbetrieb. Das Architekturmuster ist alt, und keine der einzelnen Ideen stammt von mir. Zeit sparen können höchstens die konkreten Funde und die Fehler, und darum geht es hier.
Am Anfang das negative Ergebnis, weil es mir am meisten Zeit gespart hätte, wenn ich es früher gefunden hätte.
Es gibt ein Feature namens MCP Tunnels, gebaut für genau diesen Zweck: private Netze erreichen. Als Custom Connector in claude.ai funktioniert es nicht. Es gehört zu einer anderen Produktfläche, den Managed Agents.1 Custom Connectors in claude.ai müssen aus dem Netz von Anthropic heraus über das öffentliche Internet erreichbar sein.
Damit fällt die Option weg, zu der ich zuerst gegriffen habe. Und sie verschiebt die Frage, was sich als der nützliche Teil herausgestellt hat. Statt „wie tunneln wir rein?" heißt sie nun „was ist das Schmalste, was wir exponieren können?". Die zweite Frage lässt sich deutlich leichter gut beantworten.
Das Gateway ist ein Prozess, öffentlich erreichbar, zwischen Chat-Client und internen Systemen. Alles andere bleibt von außen unerreichbar.
Die Richtung der Verbindung ist das, was das Ganze trägt. Das Gateway ruft in das private Netz hinein, der LLM-Anbieter nie. Der Anbieter sieht ausschließlich eine HTTPS-Adresse. Niemand braucht einen VPN-Zugang, und genau das ist die Alternative, die hier ersetzt wird.
Alle Pfeile zeigen in dieselbe Richtung. Es gibt keinen Pfeil vom Netz von Anthropic in das private Netz hinein, und genau darum geht es.
Die Kontrollen sind keine aus einem Framework abgeschriebene Checkliste. Jede steht dort wegen eines konkreten Angriffs:
| Kontrolle | Der Angriff dahinter |
|---|---|
Nur schmale, typisierte Tools, kein query(sql), kein http_request(url) | Das Modell kann keine Anfrage formulieren, die das Gateway nicht vorgesehen hat |
| Das Gateway hält die Credentials, nie das Modell | Aus einem Kontext, der sie nie enthalten hat, lassen sich keine Credentials extrahieren |
| Kein Token-Passthrough, das eingehende Token endet am Gateway | Confused Deputy. Die MCP-Spezifikation macht daraus ein MUST NOT2 |
| Handelnder Benutzer und Scopes kommen aus dem Token, nie aus einem Tool-Argument | Der Aufrufer kann weder eine Identität noch eine Berechtigung behaupten, indem er darum bittet |
| Tools werden nach Scope gefiltert, bevor sie angeboten werden | Ein Tool, das nicht im Kontext des Modells steht, kann eine Injection auch nicht anfordern |
| Limits für Antwortgröße und -anzahl | Der Tool-Result-Pfad ist ein Exfiltrationskanal, und hier wird er begrenzt |
| Jeder Aufruf auditiert, Argumente gehasht | Forensische Rekonstruktion, ohne sensible Daten ein zweites Mal zu speichern |
Zwei weitere, die es nicht in die Tabelle geschafft haben, aber billig zu ergänzen sind: sensible Felder gibt es nur bei Einzelabfragen und nie in Listen, und es gibt Rate Limits pro Identität und pro Tool.
Wenn ich nur eine Zeile behalten dürfte, wäre es die erste, und sie ist zugleich die, die am leichtesten weggehandelt wird. Im Design Review klingt der Handel völlig vernünftig: bauen wir doch einfach ein generisches Query-Tool ein, das ist viel flexibler. Nur ist Flexibilität hier die Eigenschaft, die man nicht haben will. Ein generisches Query-Tool gibt dem Modell eine Ausdruckssprache, und eine Ausdruckssprache deckt jeden Request ab, den man hätte verbieten wollen.
Das Bild, zu dem ich immer wieder zurückkomme, ist ein Schalter. Man geht nicht ins Lager. Man fragt am Schalter, und die Person dahinter macht die Dinge auf ihrer Liste, egal wie nett man um etwas anderes bittet. Ein VPN-Zugang ist demgegenüber die Schlüsselgewalt über das Lager.
Ein Mensch wählt im Browser eine Identität. Dieses Subject kommt unversehrt in einem System an, das der LLM-Anbieter nicht erreichen kann, und taucht im Audit-Log des Gateways ebenso auf wie im Log des internen Systems: zwei Logs, dasselbe Ereignis, von zwei Seiten.
Danach entscheidet die Identität über die Daten. Auf die Frage „welche offenen Tickets sind mir zugewiesen?" ruft der Assistent zuerst und unaufgefordert ein whoami-artiges Tool auf und filtert auf dem Ergebnis. Das Audit-Log zeigt genau diese Abfolge unter einem Subject: Identitätsabfrage, dann Ticketsuche. Gefiltert wird auf einer Identität, die der Aufrufer weder mitliefert noch wählen kann. Dass der Assistent sie von sich aus nachschlägt, ist Bequemlichkeit, keine Durchsetzung.
Es gab noch eine schöne Überraschung, mit der ich nicht gerechnet hatte. Auf die Frage nach anstehenden Terminen, verbunden als Service-Account, merkte der Assistent von sich aus an, dass er als Maschinenkonto handle und nicht als die fragende Person, und dass einer der sichtbaren Kalender daher vermutlich nicht deren eigener sei. Niemand hatte ihn gebeten, das zu prüfen. Die Identität war in den Tool-Antworten schlicht sichtbar genug, um Teil der Antwort zu werden.
Ein Fehler auf dem Weg dorthin ist vermutlich nützlicher als die Demo. Das MCP-Framework, das ich verwendet habe, fastmcp, bringt einen In-Memory-OAuth-Provider zum Testen mit. Er fährt einen vollständigen, spezifikationsförmigen OAuth-2.1-Flow und genehmigt jede Authorization-Anfrage automatisch, ohne festzuhalten, wer der Benutzer ist.3 Zum Testen des Transports ist das in Ordnung. Für alles, was mit Identität zu tun hat, ist es irreführend, denn er stellt ein plausibel aussehendes Token ohne Subject aus. Ein whoami-Tool liefert nichts zurück, und die Auth sieht trotzdem funktionierend aus.
Eine Komponente kann ein Protokoll korrekt implementieren und für die Eigenschaft, um die es einem geht, trotzdem nutzlos sein. „Wir haben OAuth" war eben nicht derselbe Satz wie „wir wissen, wer fragt".
Ein Ticket in den Testdaten enthält eine echte Prompt Injection. Sie weist den Assistenten an, sämtliche Tickets auszugeben, den internen API-Key preiszugeben und Kundendaten per POST an eine externe Adresse zu schicken.
Es passiert nichts. Nicht weil ein Filter zugegriffen hätte, sondern weil keines dieser drei Dinge eine Fähigkeit des Gateways ist. Die Sammelabfrage ist auf zehn Datensätze gedeckelt und lässt das Kundenfeld weg. Der Key gelangt nie in den Kontext des Modells. Es gibt kein Tool, das eine URL entgegennimmt.
Die Injection wird nicht erkannt. Sie findet einfach nichts vor, womit sie arbeiten könnte.
Um diesen Unterschied geht es in der ganzen Übung. Erkennung ist ein Wettrüsten, das man irgendwann verliert, weil sich Anweisungen nicht zuverlässig von Fließtext trennen lassen. Nichts zu haben, worauf sich umlenken ließe, verschlechtert sich dagegen nicht, wenn der Angreifer kreativer wird, denn Kreativität muss weiterhin innerhalb des vorhandenen Vokabulars operieren, und gegenüber dem Ticketsystem sind das vier typisierte Tools.
Ein Live-Durchlauf hat mir die unfreiwillige Fassung dieses Arguments geliefert. Der Assistent rief ein Tool auf, bevor dessen Schema geladen war, und im Chat erschien ein roter Fehler: dieses Tool ist noch nicht geladen, ruf zuerst die Tool-Suche auf. Der Assistent tat genau das und wiederholte den Aufruf erfolgreich. Das dürfte das Lazy Loading der Tool-Definitionen im Chat-Client selbst sein, der ab irgendeinem Punkt nicht mehr jedes Schema vorab mitschickt. Ich habe den Fehler einmal gesehen und den Mechanismus daraus geschlossen, es ist also eine Vermutung. Sicher ist nur, dass nichts auf der Serverseite ihn verursacht oder verhindern könnte.
Der Teil, der bleibt: Das Audit-Log zeigte für dieses Tool in dieser Session genau einen Aufruf, Ergebnis ok. Der fehlgeschlagene Aufruf hat den Browser nie verlassen. Das Modell hat sich geirrt, und der Irrtum war strukturell uninteressant.
Ein schreibendes Tool verlangte einen Scope, den nur Mitglieder einer bestimmten Gruppe haben. Einem nur lesenden Benutzer wird der Aufruf nicht bloß verweigert: Das Tool wird ihm nie angeboten, dem Modell wird also nie mitgeteilt, dass die Fähigkeit existiert.
Das sind zwei unabhängige Schichten, und die Redundanz ist Absicht. Das Tool fehlt in der Tool-Liste, und dem Token fehlt der Scope, falls es doch aufgerufen würde. Die erste Schicht betrifft den Kontext des Modells, die zweite den Request-Pfad, und keine verlässt sich darauf, dass die andere hält.
Gegen ein echtes System war die erste Entscheidung der Authentifizierungs-Flow, und meine erste Wahl war die falsche.
Nextcloud bringt eine OAuth2-App mit, die wie die naheliegende Wahl aussieht. Ich habe sie aus drei Gründen verworfen. Es gibt keine Scopes: Die Admin-Dokumentation von Nextcloud sagt, „every token has full access to the complete account including read and write permission to the stored files", und weiter, „without scopes and restrictable access it is not recommended to use a Nextcloud instance as a user authentication service."4 Wenn ein Hersteller so deutlich vom eigenen Feature abrät, hört man besser hin. Dazu kommt fehlendes PKCE, und es gibt Berichte, dass Authorization: Bearer in bestimmten Versionen an WebDAV-Endpunkten scheitert.
Was stattdessen funktioniert, ist Login Flow v2, also das, was die offiziellen Desktop- und Mobile-Clients verwenden.5 Drei Schritte: Die App schickt anonym ein POST an einen Login-Flow-Endpunkt und erhält eine Login-URL plus ein Polling-Token; der Browser geht auf diese URL und authentifiziert sich auf der gewöhnlichen Login-Seite von Nextcloud; die App pollt, bis sie Server-URL, Login-Namen und ein App-Passwort erhält.
Drei Dinge machen das zur besseren Wahl. Es braucht überhaupt kein Admin-Setup, also keinen OAuth-Client zu registrieren und keine Redirect-URI zu konfigurieren. 2FA und SSO funktionieren automatisch, weil es buchstäblich die normale Login-Seite ist. Und das App-Passwort gilt pro Gerät und ist einzeln widerrufbar, für den Benutzer sichtbar in den Sicherheitseinstellungen von Nextcloud.
Der Vorbehalt gehört direkt neben die Empfehlung: Das App-Passwort gewährt weiterhin vollen Kontozugriff. Ein anderer Flow erzeugt keine Scopes, die Nextcloud nicht hat.
Nichts davon ist dramatisch, aber es ist die Sorte Stolperstein, die auf demselben Weg wieder auftaucht.
Der Mock war zu sauber. Meine Kalenderauflistung filterte System-Collections über den Namen heraus, was gegen einen lokalen Mock-Server durchging, der ausschließlich echte Kalender zurückgab. Eine echte Instanz liefert zusätzlich die Collection home, den Scheduling-Inbox und -Outbox und den Papierkorb, und das letzte Pfadsegment von home ist schlicht der Kontoname, womit das Konto als ein nach sich selbst benannter Kalender auftauchte. Der Fix war, stattdessen auf die WebDAV-Eigenschaft resourcetype zu filtern, die keine Liste von Sonderfällen braucht. Danach habe ich den Mock absichtlich unsauberer gemacht, sodass der Test nun prüft, dass die zusätzlichen Collections herausfallen. Ein Fixture, das aufgeräumter ist als die Produktion, verdeckt genau die Fehlerklasse, die die Produktion finden wird.
Login Flow v2 verwendet die Session, die der Browser schon hat. Öffnet man die Login-URL in einem normalen Fenster, wird stillschweigend das Konto angeboten, mit dem man gerade angemeldet ist. In meinem ersten Live-Test wurde so eine Admin-Session autorisiert statt des vorgesehenen Service-Accounts, und der Flow läuft in beiden Fällen erfolgreich durch, fällt also leicht nicht auf. Vorher abmelden oder ein privates Fenster verwenden. Ebenfalls gut zu wissen: Jeder abgeschlossene Login erzeugt ein App-Passwort, das bis zum Widerruf bestehen bleibt, wer also iteriert, hinterlässt eine Spur zum Aufräumen.
Eine Seite, die gelogen hat. Die Seite „warte auf deine Anmeldung" des Gateways sagte, es sollte sich eine Login-Seite geöffnet haben, und öffnete nichts. Der Fallback-Link darunter war der einzige Pfad, der je funktioniert hat. Aufgefallen ist es mir beim Lesen des Seitentexts vor einem Live-Durchlauf, nicht durch einen Test. Tests prüfen Verhalten, und niemand hatte geprüft, ob der Satz stimmt.
Das ist der Teil, den ich in einem fremden Artikel als Erstes lesen wollen würde.
Das App-Passwort gewährt vollen Kontozugriff. Nextcloud hat keine Scopes zu vergeben, das Credential ließ sich also nicht einschränken. Einschränken konnte ich das Interface: sechs nur lesende Operationen gegen Nextcloud, überhaupt kein Schreibpfad dorthin, Limits für Größe und Anzahl, und ein Credential, das den Gateway-Prozess nie verlässt. Ich habe also nicht das Credential eingegrenzt, sondern das Interface. Wer das Gateway kompromittiert, hat das Konto. Wer das Modell kompromittiert, hat sechs lesende Aufrufe, alle protokolliert. Das sind sehr unterschiedliche Schadensradien, und sie auseinanderzuhalten ist der Punkt.
Fremden Text mit einem Vermerk „das sind nicht vertrauenswürdige Daten" zu umgeben, ist ein Hinweis, keine Grenze. Anweisungen lassen sich nicht zuverlässig von Fließtext trennen. Die eigentliche Verteidigung besteht darin, nichts zu haben, worauf sich umlenken ließe.
Der Demo-Identity-Provider ist kein Identity Provider. Keine Passwörter, und er stellt die Tokens aus, die er selbst validiert, womit die Validierung zirkulär ist. Gut genug, um zu zeigen, dass ein Subject die Strecke überlebt, und ein Beleg für sonst nichts.
Der Betriebsteil hat Proof-of-Concept-Qualität. Der Rate Limiter läuft im Prozess mit einem Worker, der State liegt im Speicher und ein Neustart setzt alles zurück, und der Tunnel für die öffentliche Erreichbarkeit rotiert seinen Hostnamen, was die Connector-Konfiguration jedes Mal ungültig macht.
Die Nextcloud-Verifikation lief außerdem gegen genau eine Instanz, Version 34.0.1, mit einem Service-Account, über ältere Versionen kann ich also nichts sagen. Und die Testsuiten prüfen, dass die beschriebenen Verhaltensweisen gegen einen laufenden Endpunkt gelten, das ist funktionales und kein adversariales Testen. Ein Penetrationstest hat nicht stattgefunden.
Der Bau ist kleiner, als das Thema klingt, und die Form ist ungefähr diese:
Der Code ist nicht der schwierige Teil. Die Entwurfsentscheidungen sind es, und für die lohnt sich Zeit, weil die Tool-Fläche darüber entscheidet, welche Dinge überhaupt passieren können. Diese Menge wird im Editor festgelegt, bevor irgendein Modell sie zu sehen bekommt. Man macht eine LLM-Integration nicht sicher, indem man das Modell zum Wohlverhalten bringt, sondern indem man Fehlverhalten nichts Interessantes mehr zu tun übrig lässt.
Wenn ihr vor etwas Ähnlichem steht, sei es die Frage, welche internen Dienste sich überhaupt zum Exponieren lohnen, wie die Tool-Fläche aussehen soll oder wie man das betreibt, ohne VPN-Zugänge zu verteilen, denken wir von Infralovers das gerne mit euch durch, gerade in regulierten oder sicherheitsbewussten Umgebungen. Und wenn ihr das Wissen lieber im Haus aufbaut, haben wir auch Kurse zu relevanten KI Themen.
Die dokumentierte Hälfte davon: „When you add a custom connector, Claude connects to your remote MCP server from Anthropic's cloud infrastructure, rather than from your local device" und „your MCP server must be reachable over the public internet from Anthropic's IP ranges." support.claude.com, gegen den Artikel geprüft. Dass MCP Tunnels nicht als Transport für Custom Connectors in claude.ai dienen, ist eine Abwesenheit, und Abwesenheiten haben keine URL: Sie stützt sich darauf, dass das Tunnel-Feature für Managed Agents dokumentiert ist und die Custom-Connector-Dokumentation keinen solchen Weg anbietet. Produktflächen ändern sich, vor einer Festlegung also erneut prüfen. ↩︎
Model Context Protocol, Spezifikation Authorization, Revision 2025-11-25. „MCP servers MUST only accept tokens that are valid for use with their own resources. MCP servers MUST NOT accept or transit any other tokens", und zu Upstream-Aufrufen: „The MCP server MUST NOT pass through the token it received from the MCP client." modelcontextprotocol.io, gegen den Spezifikationstext geprüft. Bewusst auf eine datierte Revision verlinkt, weil die Pfade draft und latest wandern. ↩︎
fastmcp 3.4.5 bringt einen In-Memory-OAuth-Provider für lokale Entwicklung und Tests mit. Beobachtetes Verhalten während dieses Aufbaus: Er fährt einen vollständigen OAuth-2.1-Authorization-Code-Flow und genehmigt jede Authorization-Anfrage automatisch, ohne eine Benutzeridentität zu erfassen, weshalb das ausgestellte Token kein Subject trägt. Das ist ein Testwerkzeug, das sich dokumentiert verhält, und kein Defekt. Der Punkt ist, dass es sich von funktionierender Authentifizierung schwer unterscheiden lässt, solange man nicht nach dem Subject sucht. Versionsabhängig, also gegen die eingesetzte Version prüfen. ↩︎
Nextcloud Admin Manual, OAuth2-Konfiguration. Beide zitierten Sätze wörtlich geprüft gegen docs.nextcloud.com; die Seite folgt dem aktuellen Release, die Formulierung kann sich also ändern. Der PKCE-Support wird unter nextcloud/server#12881 geführt, „Implement OAUTH2 Authorization code with PKCE", eröffnet im Dezember 2018 und zum Zeitpunkt des Schreibens weiterhin offen. Das Scheitern von Authorization: Bearer an WebDAV ist nextcloud/server#5512, „No 'Authorization: Bearer' header found." Dieses Issue ist geschlossen, es belegt also, dass der Fehlerfall existierte und versionsabhängig ist, nicht dass er in einem bestimmten Release vorliegt. Beide Issue-Status geprüft. ↩︎
Nextcloud Developer Manual, Login Flow v2. Anonymes POST an den Login-Flow-Endpunkt liefert eine Login-URL und ein Poll-Token; der Benutzer authentifiziert sich im Standardbrowser, inklusive 2FA, gegen eine Session mit fünf Minuten Lebensdauer; der Client pollt den Poll-Endpunkt, der bis zum erfolgreichen Login 404 liefert und danach Serveradresse, Login-Namen und ein App-Passwort. Geprüft gegen docs.nextcloud.com und gegen eine Instanz mit Version 34.0.1. ↩︎
Sie interessieren sich für unsere Trainings oder haben einfach eine Frage, die beantwortet werden muss? Sie können uns jederzeit kontaktieren! Wir werden unser Bestes tun, um alle Ihre Fragen zu beantworten.
Hier kontaktieren