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

248 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:<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:
```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 <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.
```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 <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.
```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:<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](../BACKLOG.md).