# Authentifizierung, Client-Verwaltung und Überlastschutz Betriebshandbuch für den HTTP-Betrieb. Lokal über **stdio gibt es keine Authentifizierung** und keine Limits pro Client, denn dort startet der Nutzer den Prozess selbst. - [Überblick](#überblick) - [Einrichtung im Pod](#einrichtung-im-pod) - [Clients verwalten](#clients-verwalten) (CLI und Admin-API) - [Einstellungen pro Client](#einstellungen-pro-client) - [Netzwerk-Clients und Proxys](#netzwerk-clients-und-proxys) - [Abläufe: anlegen, sperren, rotieren](#abläufe) - [Überlastschutz](#überlastschutz) - [Sicherheitshinweise](#sicherheitshinweise) - [Ausblick: Keycloak](#ausblick-keycloak) --- ## Überblick ``` ┌──────────────────────────── Pod (Replica 1..N) ────────────────────────────┐ Client ──HTTP──► │ :8080 uvicorn ─► Auth (Token/Netz) ─► Limit pro Client ─► Queue ─► Worker │ │ :9090 Admin-API (nur 127.0.0.1) │ └────────────┬──────────────────────────────────────────────────▲────────────┘ │ schreibt (Lock, atomar) │ liest bei Änderung ▼ │ (alle 5 s geprüft) /data/clients.json + /data/audit.log (RWX-PVC, alle Replicas) ``` Es gibt drei Betriebsarten: | Konfiguration | Verhalten | |---|---| | `MCP_CALC_CLIENTS_FILE` gesetzt | **Empfohlen.** Clients aus der Datei, änderbar zur Laufzeit per CLI oder Admin-API | | nur `MCP_CALC_AUTH_TOKENS=name:token,…` | einfache statische Tokens (z. B. aus einem Secret), Änderung nur per Neustart | | nichts gesetzt | keine Authentifizierung (Warnung im Log, wenn nicht an localhost gebunden) | Beide Quellen lassen sich kombinieren, die Client-Namen müssen dann eindeutig sein. Jeder Request wird einem **Client** zugeordnet. Der Name erscheint in Logs (`tool=… client=…`) und Metriken (`client`-Label). Bei Überlast ist dadurch sofort sichtbar, wer sie verursacht. --- ## Einrichtung im Pod | Einstellung | Wert | Zweck | |---|---|---| | PVC | RWX, gemountet z. B. unter `/data` | gemeinsame Client-Datei für alle Replicas | | `MCP_CALC_CLIENTS_FILE` | `/data/clients.json` | aktiviert die Authentifizierung; Datei darf anfangs fehlen (= keine Clients) | | `MCP_CALC_AUDIT_FILE` | Default `/data/audit.log` | Änderungsprotokoll | | `MCP_CALC_ADMIN_TOKENS` | `ops:` aus einem **Secret** | aktiviert die Admin-API; mehrere Admins kommagetrennt | | `MCP_CALC_ADMIN_HOST` / `_PORT` | `127.0.0.1` / `9090` | Admin-API nur im Pod erreichbar (siehe unten) | | `MCP_CALC_TRUSTED_PROXIES` | IP/CIDR des OpenShift-Routers | nur nötig für Netz-Einschränkungen hinter der Route | Admin-Token erzeugen: `python -c "import secrets; print(secrets.token_urlsafe(32))"`. **Zugriff auf die Admin-API.** Mit dem Default `127.0.0.1` ist sie nur aus dem Pod heraus erreichbar: ```bash oc port-forward deploy/mcp-calculator 9090:9090 # dann lokal: http://localhost:9090/admin/clients # oder direkt die CLI im Pod: oc exec deploy/mcp-calculator -- mcp-calculator-admin list ``` Soll ein anderer Dienst im Cluster die API nutzen, `MCP_CALC_ADMIN_HOST=0.0.0.0` setzen, einen eigenen internen Service **ohne Route** anlegen und per NetworkPolicy einschränken. **Wartung der Datei.** Alle Replicas lesen `/data/clients.json`. Geschrieben wird unter einer exklusiven POSIX-Sperre (`fcntl.lockf`, funktioniert auf NFS und CephFS) und atomar per Umbenennen, sodass nie eine halb geschriebene Datei gelesen wird. Ist die Datei nach einer Änderung ungültig (z. B. Tippfehler bei Handbearbeitung), **behalten die Replicas den letzten gültigen Stand** und loggen einen Fehler. Beim Start führt eine ungültige Datei dagegen zum Abbruch. Die Datei gehört ins Backup. --- ## Clients verwalten ### CLI ```bash export MCP_CALC_CLIENTS_FILE=/data/clients.json # im Pod bereits gesetzt mcp-calculator-admin create opencode-team-a --max-concurrent 4 --expires 2027-03-31 --note "Team A, IDE" mcp-calculator-admin create reporting-service --networks 10.130.4.0/24 mcp-calculator-admin create legacy-batch --network-only --networks 10.131.2.15 mcp-calculator-admin list mcp-calculator-admin show opencode-team-a mcp-calculator-admin disable opencode-team-a # sperren mcp-calculator-admin enable opencode-team-a mcp-calculator-admin set opencode-team-a --max-concurrent 8 --expires never --networks none mcp-calculator-admin rotate opencode-team-a # neues Token, altes sofort ungültig mcp-calculator-admin delete opencode-team-a --yes mcp-calculator-admin --json list # maschinenlesbar ``` Beispielausgabe von `list`: ``` NAME STATUS AUTH LIMIT EXPIRES TOKEN NETWORKS legacy-batch active network default - - 10.131.2.15/32 opencode-team-a active token 4 2027-03-31 mcpc_kFD5… - reporting-service disabled token default - mcpc_x81Q… 10.130.4.0/24 ``` ### Admin-API Alle Aufrufe brauchen `Authorization: Bearer `. | Methode | Pfad | Body | Ergebnis | |---|---|---|---| | `GET` | `/admin/clients` | – | Liste aller Clients (ohne Hashes) | | `POST` | `/admin/clients` | `{"name", "auth"?, "max_concurrent"?, "expires"?, "allowed_networks"?, "note"?}` | `201`, Client + **Token (einmalig)** | | `GET` | `/admin/clients/{name}` | – | ein Client | | `PATCH` | `/admin/clients/{name}` | `{"enabled"?, "max_concurrent"?, "expires"?, "allowed_networks"?, "note"?}` | geänderter Client | | `POST` | `/admin/clients/{name}/rotate` | – | Client + **neues Token (einmalig)** | | `DELETE` | `/admin/clients/{name}` | – | `{"deleted": name}` | | `GET` | `/healthz` | – | ohne Token | Fehler: `400` ungültige Eingabe, `401` fehlendes/falsches Admin-Token, `404` unbekannt, `409` existiert bereits. ```bash A="Authorization: Bearer $ADMIN_TOKEN" curl -s -H "$A" -H 'Content-Type: application/json' -X POST localhost:9090/admin/clients \ -d '{"name": "agent-reporting", "max_concurrent": 2, "expires": "2027-06-30"}' curl -s -H "$A" -H 'Content-Type: application/json' -X PATCH localhost:9090/admin/clients/agent-reporting \ -d '{"enabled": false}' ``` Die Replica, die den Admin-Aufruf bearbeitet, übernimmt die Änderung sofort. Alle anderen Replicas folgen nach spätestens `MCP_CALC_CLIENTS_RELOAD_SECONDS` (Default 5 s). ### Clients verwenden ``` Authorization: Bearer mcpc_… (Standard, z. B. opencode "headers", Cline "headers") X-API-Key: mcpc_… (Alternative für Dienste, die Authorization nicht setzen können) ``` --- ## Einstellungen pro Client | Feld | Default | Bedeutung | |---|---|---| | `auth` | `token` | `token` = Token nötig; `network` = nur über die Quell-IP (siehe unten) | | `enabled` | `true` | `false` sperrt den Client, ohne ihn zu löschen (Antwort `403 client is disabled`) | | `expires` | – | letzter gültiger Tag (UTC), danach `403 API token has expired` | | `max_concurrent` | Server-Default | gleichzeitige Berechnungen dieses Clients; `0` = unbegrenzt; nicht gesetzt = `MCP_CALC_CLIENT_MAX_CONCURRENT` | | `allowed_networks` | – | IPs/CIDRs, aus denen der Client zugreifen darf; leer = überall | | `note` | – | Freitext (Team, Ansprechpartner, Zweck) | | `token_hint` | automatisch | die ersten Zeichen des Tokens zur Wiedererkennung (`mcpc_kFD5…`) | | `created`, `updated` | automatisch | Zeitstempel | --- ## Netzwerk-Clients und Proxys Für einen anderen Dienst gibt es zwei Möglichkeiten: 1. **Token mit Netz-Einschränkung (empfohlen):** `create reporting-service --networks 10.130.4.0/24`. Das Token gilt nur aus diesem Netz. Ein gestohlenes Token ist von anderswo wertlos. 2. **Nur Netz** (`--network-only`): kein Token, Zugang allein über die Quell-IP. Das ist bequem, aber schwächer: - In OpenShift wechseln Pod-IPs. Ein Netz muss daher den ganzen Bereich abdecken, aus dem der Dienst kommt. - Hinter der Route sieht der Server nur die IP des Routers. Die echte Client-IP steht in `X-Forwarded-For`, und der Server wertet sie **nur** aus, wenn der direkte Absender in `MCP_CALC_TRUSTED_PROXIES` steht. Sonst könnte jeder den Header fälschen. - Innerhalb des Clusters (Aufruf über den Service, ohne Route) sieht der Server die echte Pod-IP. - Ergänzend sollte eine NetworkPolicy den Zugriff auf Namespace- oder Label-Ebene begrenzen. Reihenfolge der Prüfung: Wird ein Token mitgeschickt, muss es gültig sein. Ein falsches Token wird auch aus einem freigegebenen Netz abgelehnt. Nur ohne Token greifen Netzwerk-Clients, bei mehreren Treffern gewinnt das spezifischste Netz. Signierte Header (HMAC) sind vorgesehen, aber nicht umgesetzt, siehe [BACKLOG.md](../BACKLOG.md). --- ## Abläufe **Neuer Nutzer oder Dienst:** `create`, das Token sicher übergeben (Passwort-Manager, Secret) und im Client als Header eintragen. Das Token lässt sich nicht erneut anzeigen. Geht es verloren: `rotate`. **Sofort sperren** (z. B. Token geleakt oder Agent läuft Amok): `disable `. Das wirkt auf der bearbeitenden Replica sofort, auf allen anderen nach spätestens 5 s. Danach `rotate` und das neue Token ausgeben, zum Schluss `enable`. **Regelmäßig rotieren:** `expires` setzen. Vor Ablauf `rotate`, dann `set --expires `. **Überblick:** `list` bzw. die Metriken `mcp_calc_clients{status}` und `mcp_calc_auth_failures_total{reason}`. Häufige `invalid_token`-Fehler deuten auf falsch konfigurierte Clients oder Rateversuche hin. **Audit:** `/data/audit.log`, eine JSON-Zeile pro Änderung, z. B. ```json {"time": "2026-09-11T17:30:42+00:00", "actor": "admin:ops", "action": "create", "client": "demo", "details": {"max_concurrent": 2, "token_hint": "mcpc_cv1k"}} ``` `actor` ist `admin:` (Admin-API) oder `cli:` (CLI). Tokens und Hashes werden nie protokolliert. --- ## Überlastschutz Mehrere Schutzschichten greifen nacheinander. Jede lehnt früh und billig ab, statt Last aufzustauen: | # | Schicht | Einstellung (Default) | Wirkung bei Überschreitung | Metrik-Status | |---|---|---|---|---| | 1 | Verbindungen | `MCP_CALC_MAX_CONNECTIONS` (200) | HTTP `503` von uvicorn | – | | 2 | Limit pro Client | `MCP_CALC_CLIENT_MAX_CONCURRENT` (10) bzw. `max_concurrent` | Tool-Fehler „client busy“ | `busy` | | 3 | Gesamtkapazität | `MCP_CALC_WORKERS` + `MCP_CALC_MAX_QUEUE` (2 + 20) | Tool-Fehler „server busy“ | `busy` | | 4 | Wartezeit in der Queue | `MCP_CALC_MAX_QUEUE_WAIT_SECONDS` (30) | Tool-Fehler „no worker became free“ | `busy` | | 5 | Rechenzeit | `MCP_CALC_TIMEOUT_SECONDS` (10) | Worker wird beendet, Tool-Fehler „timeout“ | `timeout` | | 6 | Verbindungsabbruch | immer aktiv | wartende Berechnung verworfen, laufende abgebrochen (Worker beendet) | `cancelled` | Die Ablehnungen der Schichten 2–5 sind **Tool-Fehler mit Hinweis „retry“**. Das LLM bzw. der Agent sieht, dass es später erneut versuchen soll. Schicht 6 verhindert, dass Berechnungen weiterlaufen, auf deren Ergebnis niemand mehr wartet. Alle Zähler gelten **pro Replica**. Das passt zum Ziel Überlastschutz: Jede Replica schützt sich selbst, ein gemeinsamer Zähler ist nicht nötig. Mit mehr Replicas steigt die Gesamtkapazität linear. **Hinweise zur Dimensionierung:** - `CLIENT_MAX_CONCURRENT` kleiner als `WORKERS + MAX_QUEUE` wählen, sonst kann ein einzelner Client alles belegen. - `MAX_QUEUE_WAIT_SECONDS` unter dem Timeout der Clients halten, sonst warten Anfragen, die niemand mehr abholt. Die Schicht 6 fängt das zusätzlich ab. - Alarm z. B. auf `mcp_calc_inflight_computations / mcp_calc_capacity > 0.8` oder auf `rate(mcp_calc_tool_calls_total{status="busy"}[5m]) > 0`, siehe README, Abschnitt Monitoring. - Lastspitzen einzelner Clients erkennt man an `mcp_calc_client_inflight_computations{client}` und `mcp_calc_tool_calls_total{client, status="busy"}`. --- ## Sicherheitshinweise - **Nur Hashes gespeichert:** Wer die Datei lesen kann, kann damit nicht zugreifen, aber Clients anlegen oder entsperren, wenn er schreiben kann. PVC-Zugriff daher auf den Pod beschränken. Die Datei wird mit Rechten `0640` geschrieben. - **Tokens** sind 256 Bit zufällig (`mcpc_` + 43 Zeichen). Admin-Tokens mindestens ebenso lang wählen (Mindestlänge 16 Zeichen wird erzwungen). - **Admin-API** nie über eine Route veröffentlichen. Default ist `127.0.0.1`. - `/healthz`, `/readyz` und `/metrics` sind ohne Token erreichbar. Sie enthalten keine Rechen-Eingaben, `/metrics` aber Client-Namen. Bei Bedarf per NetworkPolicy einschränken oder `MCP_CALC_METRICS=false` setzen. - Abgelehnte Anfragen werden mit Grund, Client (falls bekannt) und IP geloggt (`auth rejected: reason=…`). --- ## Ausblick: Keycloak Geplant ist die Prüfung von Keycloak-JWTs (OIDC) über den `TokenVerifier` des MCP SDK, zusätzlich zu den API-Tokens. Die Identität aus dem JWT (z. B. `azp`/Client-ID oder ein Claim) wird dann genauso wie heute ein Client behandelt: gleiche Limits, gleiche Metriken. API-Tokens bleiben für Dienste und Clients, die kein OIDC können. Offene Frage vor der Umsetzung: ob opencode und Cline den OAuth-Ablauf von MCP gegen Keycloak unterstützen. Details siehe [BACKLOG.md](../BACKLOG.md).