Sicherheit¶
Diese Seite ist das, was eine Sicherheitsprüfung im Zuschnitt von HIPAA oder SOC 2 ausgehändigt bekommt: die Vertrauensgrenzen, welche Daten das Deployment verlassen und an wen, was wo verschlüsselt ist, und eine Kontrollmatrix, die für jede Kontrolle den Mechanismus in diesem Code benennt, der sie erfüllt, und den Test, der sie hält.
Sie beschreibt, was ist, nicht was schön wäre. Eine Zeile ohne Mechanismus
sagt das und verlinkt das Issue, das ihn bauen würde. Wie man eine Schwachstelle
meldet und wie die Härtungs-Checkliste für die Produktion aussieht, steht in
SECURITY.md im
Wurzelverzeichnis des Repositorys; diese Seite ist alles andere, in einer Kopie.
Zwei benachbarte Seiten beantworten die Fragen, die eine Prüfung als Nächstes stellt, und werden hier nicht wiederholt: Datenschutz dazu, wo personenbezogene Daten liegen, was eine Löschung tatsächlich erreicht und welche Lücken noch offen sind, und Lizenzen zu jeder Drittkomponente, die die Images ausliefern.
Bedrohungsmodell¶
Die Plattform ist selbst gehostet und mandantenfähig. Die Entwurfsannahme ist, dass die Infrastruktur des Betreibers vertrauenswürdig ist und jede Anfrage in sie hinein nicht — die Grenzen, auf die es ankommt, sind also jene, die eine Anfrage auf ihrem Weg zu den Daten überschreitet.
| Grenze | Was sie überschreitet | Auf der anderen Seite vertrauenswürdig? |
|---|---|---|
| Browser → BFF (die Next.js-Route-Handler) | Ein Session-Cookie, der Organisations-Header, Formulareingaben | Nein — aber das BFF prüft das Cookie nicht: es liest das httpOnly-access_token und reicht es als Bearer-Header weiter (frontend/src/lib/platform-proxy.ts). Es ist eine Grenze, die ein Credential weiterreicht; die Prüfung ist Aufgabe der API |
| BFF → API (FastAPI) | Ein an eine DB-Session gebundenes JWT, der Header X-Organization-Id |
Nein — das Token wird pro Anfrage geprüft, die Session auf Widerruf kontrolliert und die Organisation aus dem Token aufgelöst |
| API → PostgreSQL / Redis | Queries und Cache-Lesezugriffe, über TLS, wenn konfiguriert | Ja — der Speicher gehört dem Betreiber; was er im Ruhezustand schützt, steht unter „Was wo verschlüsselt ist“ |
| API / Worker → Modell-Provider, Kanäle, MCP-Server, Suchanbieter, Logfire | Prompts, Tool-Aufrufe, Anfragen, Antworten, Traces | Nein — das sind Dritte; was sie erreicht, ist eine Entscheidung pro Agent, außer beim deploymentweiten Tracing (unten) |
| Worker → Konnektoren (Google Drive, S3, …) | Aus dem Vault entsiegelte Credentials, abgerufene Dokumente | Nein — ein Konnektor-Credential ist ein Secret im Vault, referenziert über seine id |
Autorität innerhalb eines Mandanten ist nie ein Rollenname auf einer Route: sie
ist eine Mitgliedschaftszeile plus der Berechtigungskatalog
(app/core/permissions.py), pro Ressource aufgelöst. Zwei Aufrufer mit derselben
Rolle können unterschiedliche Zeilen erreichen, weil ein Grant auf einer Ressource
erweitert, was eine Rolle erlaubt, ohne das Mitglied zu befördern.
Was das Deployment verlässt¶
Nichts funkt nach Hause. Jeder ausgehende Aufruf ist einer, den das Deployment konfiguriert hat, und jeder ist eine Grenze, nach der die Prüfung eines Kunden fragen wird.
| Wohin | Was | Wann |
|---|---|---|
| Der konfigurierte Modell-Provider | Der Prompt, die Ausgabe des Modells, Tool-Argumente und -Ergebnisse | Jeder Run — außer das Modell läuft auf der eigenen Infrastruktur des Betreibers, dann verlässt nichts das Deployment |
| Der konfigurierte Kanal (Slack, Telegram, Mattermost) | Die generierten Antworten des Agents — Text, Bilder und Anhänge | Immer wenn ein Agent über diesen Kanal exponiert ist; jedes send_message postet beim Provider (app/services/channels/) |
| Logfire | Traces, die standardmäßig Prompts und Ausgaben tragen | Zwei unabhängige Pfade. Ein Observability-Token pro Agent traced diesen Agent; ein deploymentweites LOGFIRE_TOKEN instrumentiert jeden Run global (app/core/logfire_setup.py), mit ihm verlassen Run-Inhalte das Deployment also unabhängig von jeder Einstellung pro Agent. Der content-Modus dieses Tokens entscheidet, wie viel der Span trägt - none reduziert ihn auf Zeit, Tokens, Kosten und Tool-Namen (#1413). Ein gefiltertes Dazwischen ist #1616 |
| MCP-Server | Tool-Aufrufe und ihre Argumente | Nur für die Tools, an die ein Agent gebunden ist |
| Ein Websuche-Anbieter (Tavily, DuckDuckGo) | Die Suchanfrage | Nur wenn die Such-Capability gewährt ist |
| Ein Embedding-Provider | Dokumenttext, beim Ingest | Nur für eine Wissensbasis, deren Provider entfernt ist |
Was wo verschlüsselt ist¶
Es gibt einen Verschlüsselungsmechanismus auf Anwendungsebene, und er ist
bewusst der einzige: der Vault (app/core/vault.py). Jedes Konnektor- und
API-Credential im Ruhezustand ist in einem Umschlag pro Eigentümer versiegelt,
dessen Wrapping-Key über HKDF aus der Organisation (oder dem Benutzer) abgeleitet
wird, zu der es gehört — ein Chiffrat, das in die Zeile einer anderen
Organisation kopiert wird, lässt sich also nicht entsiegeln. Der Master-Key ist
rotierbar, ohne die Payloads neu zu verschlüsseln.
Nicht alles, was die Plattform speichert, ist ein Credential im Vault, und das steht hier klar, weil eine Prüfung es findet:
- Kurzlebige Bearer-Token — Organisationseinladungen
(
OrganizationInvitation.token), Kanal-Verknüpfungsanfragen (ChannelLinkRequest.token) und Freigabelinks für Konversationen (ConversationShare.share_token) — sind zufälligeString(64)-Spalten, die über Gleichheit nachgeschlagen werden, nicht im Vault versiegelt. Wer den Wert hat, kann ihn nutzen, sie sind also durch Ablauf und Einmalverwendung geschützt, nicht durch Verschlüsselung. Session-Refresh-Token sind die Ausnahme, die im Ruhezustand gehasht ist (sessions.refresh_token_hash). - Hochgeladene Dateien und Chat-Dateien liegen im Klartext auf dem
Dateisystem des API-Containers (
app/services/file_storage.py) — geschützt nur durch Volume-Verschlüsselung. - Nachrichteninhalte,
rag_documentssamt Vektoren und Sandbox-Workspaces werden als Klartextspalten, pgvector-Zeilen und Workspace-Dateien gespeichert. Der Vault versiegelt Credentials, keine Inhalte; der Schutz im Ruhezustand ist hier auf Datenträgerebene.
Ein S3-kompatibles Datei-Backend mit serverseitiger Verschlüsselung ist die Antwort auf Anwendungsebene für Object Storage und wird in #1423 verfolgt.
Kontrollmatrix¶
Eine Zeile pro Kontrolle, der Mechanismus, der sie erfüllt, und der Test, der sie hält. Eingeordnet gegen die technischen Schutzmaßnahmen nach HIPAA §164.312 und SOC 2 CC6–CC8.
Zugriffskontrolle · HIPAA §164.312(a) · SOC 2 CC6¶
| Kontrolle | Mechanismus | Gehalten von |
|---|---|---|
| Mandantentrennung, auch wenn der Aufrufer die Zeile besitzt | resolve_access verweigert eine Ressource mit abweichender organization_id vor der Eigentumsprüfung (app/services/access.py) |
test_resource_access.py::TestTenantBoundary, test_conversation_tenant_isolation.py, test_platform_flows.py |
| Eine Berechtigung auf jeder Collection-Route | Route-Dependency require(*perms) auf Listen-, Erstellungs- und Katalogrouten (app/api/deps.py), Katalog in app/core/permissions.py |
test_platform_routes.py::TestEachRouteDemandsItsOwnPermission |
| Routen pro Ressource autorisieren im Service, nicht auf der Route | Eine Route, die auf einem Agent, Skill oder einer Collection arbeitet, trägt kein require()-Gate — ein Rollen-Gate würde einen Grant-Inhaber abweisen, bevor der Grant greift — und ruft stattdessen resolve_access (app/services/access.py) |
test_platform_routes.py::TestEveryPlatformRouteIsGuarded (jede Route ist gegated oder wird im Service entschieden) |
| Ein Grant erweitert den Zugriff, ohne das Mitglied zu befördern | resolve_access pro Ressource nimmt max(Rollen-Scope, Grant) (app/services/access.py) |
test_resource_access.py::TestGrantsWidenAccess, ::TestPermissionsGrantsCannotWiden |
| Eine Kanal-Erwähnung läuft als der Absender, nicht als der Bot | Der eigene AuthContext eines verknüpften, aktiven Absenders wird verwendet (app/services/channels/mentions.py) |
test_channel_mentions.py::TestAnswer::test_the_run_carries_the_senders_own_role |
Authentifizierung · HIPAA §164.312(d) · SOC 2 CC6¶
| Kontrolle | Mechanismus | Gehalten von |
|---|---|---|
| JWT (HS256), bcrypt-Passwörter | app/core/security.py — verify_token, get_password_hash |
test_security.py, test_auth.py |
| API-Keys in konstanter Zeit verglichen | secrets.compare_digest (app/api/deps.py) |
test_auth.py, HMAC-Prüfungen der Webhooks in den Kanal-Adaptern |
| DB-gestützte Sessions mit Widerruf | Tabelle sessions + SessionService; Token an einen sid-Claim gebunden (app/services/session.py, app/api/routes/v1/sessions.py) |
test_session_verify.py, test_session_revocation.py |
| Rate-Limiting beim Login | enforce_auth_limit (app/api/deps.py) |
test_auth_rate_limit.py |
Audit-Kontrollen · HIPAA §164.312(b) · SOC 2 CC7¶
| Kontrolle | Mechanismus | Gehalten von |
|---|---|---|
| Governance-relevante Mutationen werden in der Transaktion der Anfrage festgehalten | record_audit (app/core/audit.py) im mutierenden Service — Secret-Rotation, Skill-/Sync-/MCP-Bindung, Mitgliedschaft, Freigabe, Freigaben, Exporte und mehr; geschrieben nach app_admin_audit_logs. Es ist keine flächendeckende Abdeckung jedes Schreibvorgangs (das CRUD der Wissensbasis etwa wird nicht auditiert) |
test_skill_binding_audit.py, test_sync_source_audit.py |
| Die Spur ist für einen Auditor lesbar | GET /audit, gegated auf audit:read (app/services/audit.py) |
test_audit_service.py |
| Export der Spur (CSV/JSONL) | GET /audit/export über ein Fenster, auf audit:read gegated, hält den eigenen Abruf in der Spur fest; die Run-, Freigabe- und Spend-Exporte tun dasselbe (#1422) |
test_exporting.py (der Export und sein eigener Audit-Eintrag) |
| Manipulationsnachweis (eine Hash-Kette) | Noch nicht — #1622 | — |
Integrität · HIPAA §164.312(c) · SOC 2 CC8 (Change Management)¶
| Kontrolle | Mechanismus | Gehalten von |
|---|---|---|
| Ein Spec wird beim Veröffentlichen abgewiesen, nie zur Laufzeit | validate_spec (app/services/agent_registry.py) — unbekannte Capability, nicht gewährter Scope, secret_id der falschen Art oder aus einer anderen Organisation, eine persönliche MCP-Verbindung |
test_agent_registry.py, test_capability_secrets.py::TestPublishValidation |
| Ein Budget wird vor der Modellanfrage geprüft, und Kosten werden auch bei einem Fehler festgehalten | BudgetGuard.wrap_model_request gated vor dem Aufruf (app/agents/capabilities/budget/); die Kosten des Runs werden in einem abschließenden finally geschrieben (app/services/agent_runner.py) |
test_spend.py::TestBudgetGuard, test_agent_runner.py::…::test_a_failed_run_still_records_its_cost |
| Eine Freigabe wird genau einmal entschieden | ApprovalService.decide weist eine nicht mehr ausstehende Zeile ab, die for_update gelesen wurde (app/services/approvals.py) |
test_approvals_queue.py::TestDecidingTwiceIsRefused |
Vertraulichkeit von Credentials · HIPAA §164.312(a)(2)(iv)¶
| Kontrolle | Mechanismus | Gehalten von |
|---|---|---|
| Kein Secret im Klartext in einer API-Antwort oder einem Audit-Eintrag | SealedStr/CredentialStr maskieren jedes repr; Hinweise sind nur die letzten 4 Zeichen (app/core/secret_kinds.py, app/core/vault.py) |
test_no_secret_escapes.py (durchkämmt die gesamte OpenAPI-Oberfläche), test_capability_secrets.py::TestInjection |
| Logs sind nicht Teil dieser Zusage | Eine fehlerhafte MCP-OAuth-Token-Antwort erreicht die Logs über einen Pydantic-ValidationError, der seine Eingabe wiedergibt — eine bekannte Lücke, #1626 |
test_mcp_connections.py::test_an_unreadable_token_response_does_not_echo_its_input (hält fest, dass das Token in caplog landet) |
| Ein Credential ist im Ruhezustand an seine Organisation gebunden | HKDF-Umschlag pro Eigentümer (app/core/vault.py); der Geltungsbereich sind Konnektor- und API-Credentials — zu den Bearer-Token, die er nicht abdeckt, siehe „Was wo verschlüsselt ist“ |
test_secret_tenant_isolation.py, test_vault.py |
Übertragungssicherheit · HIPAA §164.312(e) · SOC 2 CC6¶
| Kontrolle | Mechanismus | Gehalten von |
|---|---|---|
| TLS zu PostgreSQL und Redis | POSTGRES_SSLMODE, REDIS_SSL (app/core/config.py); doctor meldet den Live-Zustand von Postgres aus pg_stat_ssl |
Postgres, an einer echten Verbindung: test_store_tls.py; Redis, beim Bau der URL und in doctor: test_config.py, test_doctor_sandbox.py |
| Framing- und MIME-Header auf jeder Antwort; CSP auf allen außer den API-Referenz-Endpunkten | SecurityHeadersMiddleware (app/core/middleware.py), dessen exclude_paths die CSP fallen lassen — nicht Framing oder MIME — für OpenAPI, Swagger und ReDoc; dazu die eigene CSP des Frontends pro Deployment (frontend/src/middleware.ts) |
test_security_headers.py, inkl. test_an_excluded_path_keeps_its_framing_but_drops_the_csp |
| HTTPS und HSTS | Am Reverse Proxy terminiert — die mitgelieferte nginx/nginx.conf setzt HSTS; die Anwendung bewusst nicht |
Sache des Deployments; siehe die Härtungs-Checkliste |
| Rate-Limits auf öffentlichen Oberflächen | Redis-gestützte Limits auf der Run-API, dem Embed-Widget und den gehosteten Seiten (app/services/rate_limit.py); Limits pro Absender auf Kanal-Bots (app/services/channels/router.py) |
test_rate_limited_surfaces.py; das Limit der Kanal-Bots ist implementiert, aber dünn getestet |
Die Refusals als Menge¶
Die Refusal-Tests oben tragen den security-Marker. make test-security führt
die ganze Menge aus, und CI veröffentlicht die gesammelte Liste bei jedem
Backend-Lauf als Artefakt security-tests.txt (#1417) — die Refusals lassen sich
also zählen und lesen, statt geglaubt zu werden. Ein Test, dessen Name oder Modul
einen Mandanten, eine Berechtigung, ein Budget, eine Freigabe, ein Secret oder
Klartext erwähnt und den Marker nicht trägt, lässt
tests/test_security_marker.py fehlschlagen, was die Liste vollständig hält,
während die Suite wächst.
Fazit¶
- Vertraue der Infrastruktur des Betreibers; vertraue keiner Anfrage in sie hinein. Die Grenzen, auf die es ankommt, sind Browser → BFF → API → Speicher und API/Worker → Dritte. Das BFF reicht das Session-Cookie weiter; die API ist der Ort, an dem eine Anfrage geprüft wird.
- Die einzigen Daten, die das Deployment verlassen, sind die, deren Weggang es konfiguriert hat — Modell-Provider, Kanäle, MCP-Server, Such- und Embedding-Anbieter und Logfire; ein deploymentweites Logfire-Token traced den Inhalt jedes Runs.
- Konnektor- und API-Credentials sind pro Organisation im einen Vault versiegelt; kurzlebige Bearer-Token und Inhalte im Ruhezustand (Dateien, Nachrichten, RAG, Sandboxes) sind es nicht, und #1423 ist die Antwort auf Anwendungsebene für Object Storage.
- Jede Kontrolle in der Matrix benennt einen Mechanismus und einen Test — und im selben Atemzug ihre Lücken: Manipulationsnachweis, Dateiverschlüsselung auf Anwendungsebene und ein gefiltertes Dazwischen für Traces verlinken je das Issue, das sie bauen würde.
- Schwachstellen meldest du und die Härtungs-Checkliste führst du aus über
SECURITY.md; lies Datenschutz und Lizenzen neben dieser Seite.