248 lines
13 KiB
Markdown
248 lines
13 KiB
Markdown
# 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).
|