mcp-calculator/docs/auth.md
2026-09-15 20:38:16 +02:00

13 KiB
Raw Permalink Blame History

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

                  ┌──────────────────────────── 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:<langes-zufälliges-token> 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:

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

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 <admin-token>.

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.

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.


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 <name>. 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 <neues Datum>.

Ü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.

{"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:<name> (Admin-API) oder cli:<user> (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.