Zum Inhalt

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-agenticos_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)
Browser ↔ Realtime-Provider (ein Sprachanruf über WebRTC) Das Mikrofon-Audio des Anrufers und die gesprochene Antwort des Agents Nein — der Browser hält kein Credential des Providers: Der Server beantwortet sein SDP-Angebot, ein Angebot, das einen Datenkanal öffnet, wird abgelehnt, und es wird nie ein Client-Secret ausgestellt. Tools, Freigaben und Transkripte laufen über die Sideband-Verbindung des Servers
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
Ein Visual oder eine App → die Konsole Eine Bitte, einen Link zu öffnen oder im Namen der Person eine Folgenachricht zu senden Nein — die Seite ist vom Modell geschriebenes Skript, das Feindseliges gelesen haben kann. Ein Link öffnet sich und eine Folgenachricht wird gesendet erst, nachdem ein Mensch sie gelesen und geklickt hat, im eigenen DOM der Konsole; eine Folgenachricht ohne Benutzerinteraktion dahinter wird verworfen
Worker → öffentliche Websites (der web-Konnektor) GET-Anfragen nach Seiten, Sitemaps und robots.txt; kein Credential Nein — die Start-URL tippt ein Mandant ein, und jeden Link danach wählt die Website, also wird jede Anfrage und jede Weiterleitung auf SSRF geprüft, an der geprüften Adresse angewählt (app/core/pinned_http.py) und bleibt auf einem Host

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, Teams, 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 Prompts und Ausgaben tragen, sofern der Agent nichts anderes sagt Zwei unabhängige Pfade. Ein Observability-Token pro Agent traced diesen Agent, und sein content-Modus entscheidet, wie viel der Span trägt - none reduziert ihn auf Zeit, Tokens, Kosten und Tool-Namen (#1413). Ein deploymentweites LOGFIRE_TOKEN instrumentiert jeden Run, in der API wie im Worker (app/core/logfire_setup.py), mit ihm verlässt also der Inhalt jedes Agenten das Deployment, der nicht none verlangt hat; ein Agent, der es verlangt hat, wird auch auf diesem Tracer an eine inhaltsfreie Instrumentierung geheftet (suppress_content), der Modus hält also auf beiden Pfaden, und ein Spezialist dieses Agenten erbt ihn - inline geschrieben oder mitten im Run erfunden. Eine Lücke, die er nicht abdeckt: ein fehlgeschlagenes Anheften, das protokolliert und belassen wird. Keiner der beiden Pfade ist standardmäßig an. Ein gefiltertes Dazwischen gibt es bewusst nicht - ein teilweise bereinigter Export ist eine Zusicherung, die niemand prüfen kann (#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
Der Kartenkachelserver, den VISUAL_MAP_TILES nennt Von diesem Deployment, nie von einer Seite: die Koordinaten jeder Kachel, die eine Karte zeigt und die noch nicht im Cache liegt Nur wenn die Einstellung gesetzt ist und jemand eine Karte öffnet. Eine Seite schickt dem Deployment drei Zahlen, jede gegen ihre Zoomstufe geprüft; Adresse und Schlüssel des Servers bleiben auf dem Server (Apps)
Ein Realtime-Sprach-Provider (OpenAI, Azure OpenAI, Google, xAI) Während eines Sprachanrufs: das Audio des Anrufers, die Anweisungen des Agents, Tool-Definitionen und -Ergebnisse sowie die früheren Züge der Unterhaltung als Text, wenn ein Anruf beginnt oder sich neu verbindet Nur für einen Agent, dessen Abschnitt „Sprachgespräche“ diesen Provider benennt, solange ein Anruf offen ist. Hier wird kein Audio gespeichert; was der Provider behält, regelt Ihre Vereinbarung mit ihm
Ein Speech-to-Text-Provider Eine Aufnahme: eine Sprachnachricht aus einem Kanal, ein Diktat in /chat oder die eines Besuchers auf einer gehosteten Seite oder einem Widget, die auf server stehen Immer wenn eine transkribiert wird. Das Diktat eines Besuchers ist anonym und liegt außerhalb jedes Modell-Budgets, begrenzt wird es also durch das Rate-Limit des Links mal das längste Diktat. Standardmäßig ist ein Diktat rohes PCM, dessen Samples gezählt und über dem Limit vor dem Senden abgelehnt werden, diese Grenze ist also exakt; ein Deployment, das komprimiertes Audio annimmt, liest die Länge stattdessen aus dem Header der Datei, den eine von Hand gebaute Datei zu kurz angeben kann - Kanäle rechnet den schlimmsten Fall in beiden Fällen durch. Bei browser schickt der Browser des Besuchers das Audio stattdessen an seinen eigenen Anbieter, und nichts geht hier durch

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ällige String(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 dort, wohin FILE_STORAGE_BACKEND sie legt (app/services/file_storage.py). Bei local, der Voreinstellung, ist das im Klartext das Dateisystem des API-Containers, geschützt nur durch Volume-Verschlüsselung. Bei s3 sind es Objekte in einem Bucket, den das Deployment benennt, und worum jeder Schreibvorgang bittet, entscheidet FILE_STORAGE_S3_ENCRYPTION: sse-s3 als Voreinstellung oder sse-kms unter einem Schlüssel, den der Kunde kontrolliert — oder none, das gar keinen Verschlüsselungs-Header sendet und für einen S3-kompatiblen Speicher ohne KMS dahinter existiert. Bei none sind die Objekte genau so verschlüsselt, wie dieser Speicher es tut, und nicht mehr. agenticos cmd doctor gibt aus, auf welchem Backend ein laufendes Deployment steht.
  • Nachrichteninhalte, rag_documents samt 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.

Das S3-Backend ist eine Entscheidung zur Deployment-Zeit und migriert nicht, was das lokale bereits hält; die Einstellungen stehen in der Konfiguration, die Begründung in Dateiverarbeitung.

Was über eine Person gehalten wird, und was damit geschieht

DSGVO Art. 15 fragt, was Sie über jemanden halten, und Art. 17 verlangt, es zu entfernen. Beides beantwortet diese Tabelle, mit Absicht: ein Inventar, das eine Tabelle für den Export nennt und beim Löschen vergisst, ist schlechter als keines, weil es sich vollständig liest.

Die Linie, die es zieht, ist über jemanden gegen erstellt von jemandem. Was über eine Person ist, geht mit ihr; was sie für die Organisation erstellt hat — ein Agent, auf dem das Team arbeitet, eine Wissensdatenbank, ein gespeichertes Credential — wird weitergereicht, denn das Konto einer Kollegin zu löschen darf nicht den Agenten löschen, von dem das Team abhängt.

Tabelle Beim Löschen Im Export Warum
users Gelöscht Profil, ohne den Passwort-Hash Das Konto selbst
conversations, messages, tool_calls Kaskade, außer den Chats eines Projekts, die beim Team bleiben Von ihnen begonnene Threads, mit jeder Runde darin; im Chat eines Projekts, wo immer sie geschrieben haben, nur ihre Runden und die Antworten darauf Ihre eigenen, und ein Transkript ohne jede zweite Runde beantwortet nichts. Der Chat eines Projekts gehört mehreren Personen, und die Runden der anderen Mitglieder sind deren
chat_files Kaskade; die Bytes werden nach dem Commit gelöst Nicht aufgeführt Die Zeile kaskadiert von der Nachricht, die Datei auf der Platte nicht - also werden die Pfade vor dem Löschen eingesammelt und nach dessen Commit gelöst, sonst überleben die Bytes die Anfrage, die sie entfernen sollte: für nichts erreichbar und von niemandem gelöscht. Das Transkript im Export nennt den Anhang; die Bytes sind herunterladbar, solange das Konto besteht, und werden nicht mit eingepackt
message_ratings Kaskade Ja Eine geäußerte Meinung
sessions Kaskade Gerät, Adresse und Zeiten — nie das Credential, das ein Hash ist Wo sie sich angemeldet haben
api_keys, oauth_grants Kaskade, samt den Codes und Tokens, gegen die eine Zustimmung eingetauscht wurde Schlüssel, die sie ausgestellt haben, und Clients, die sie in ihrem Namen handeln ließen: Name, Präfix, Berechtigungen und Daten — nie ein Schlüssel, ein Hash oder ein Token Ein Credential, das in ihrem Namen handelt, gehört zu ihren Daten; die eigenen Assistentenschlüssel der Plattform pro Durchlauf werden nicht aufgeführt
conversation_favourites, dashboard_layouts, dashboard_presets, user_slash_commands Kaskade Layouts und Kurzbefehle Persönliche Einstellungen, für niemanden sonst bedeutsam
agent_memory_files (owner_key = person:<id>) Ausdrücklich bereinigt, unter einer Sperre, die ein gleichzeitiger Schreibvorgang ebenfalls nimmt Ja Ein String-Schlüssel ohne Fremdschlüssel: nichts kaskadierte, jede Notiz überlebte das Konto — und ein Run, der währenddessen schreibt, hätte sie neu angelegt
agent_workspaces (scope = user) Ausdrücklich bereinigt Nein owner_ref ist aus demselben Grund ein String wie owner_key, also folgt ihm keine Kaskade, und ein zustandsgestützter Workspace hält die Dateien selbst
organization_members Kaskade Welche Organisationen, als was, seit wann Eine Zeile, die sie direkt benennt und die eine Administratorin ohnehin sieht
channel_identities Ausdrücklich bereinigt Ja SET NULL ließ die Zeile mit einer Slack-ID, einem Benutzernamen und einem Anzeigenamen einer Person zurück, die es nicht mehr gibt, verknüpft mit niemandem
agent_runs SET NULL — behalten Runs, die sie gestartet haben, und was jeder kostete Ausgaben sind die Aufzeichnung der Organisation; ein anonymer Run zählt weiter für den Monat
agents, knowledge_bases, skills, contexts, agent_triggers, agent_environments, agent_exposures, local_services SET NULL — behalten Nein Für die Organisation erstellt. Sie zu entfernen nähme die Arbeit des Teams mit der Person mit
organization_secrets SET NULL, ein privates Secret steigt zur Organisation auf Nein Ein Credential, auf dem die Organisation läuft. Der Aufstieg ist, was ein herrenloses Secret daran hindert, die Löschung zu blockieren
organizations Persönliche Org gelöscht; eine geteilte, die sie erstellt haben, geht an einen anderen Owner Nein Niemand darf mit einer Organisation ohne Owner zurückbleiben
resource_grants Kaskade beim Empfänger; SET NULL beim Erteilenden Nein Eine Berechtigung für sie ist ihre; eine, die sie erteilt haben, ist die Aufzeichnung der Organisation darüber, wer was darf
app_admin_audit_logs Behalten, Akteurs-ID bleibt Nein Die Aufzeichnung der Organisation über das, was in ihr getan wurde. Ein Eintrag, der ein gelöschtes Konto nennt, ist ehrlich; einer mit entferntem Akteur ist schlimmer als nutzlos, und er gehört nicht der Person zum Mitnehmen
embed_visitors Unberührt Nein Ein Besucher ist ein Browser-Schlüssel, nie ein Konto — hier steht nichts über eine Person mit Login

Was die Löschung nicht erreicht — und die Doku sagt es, statt dass ein Deployment es herausfindet

Ein konfigurierter externer Gedächtnisspeicher. mem0 hält das, was ein Agent sich gemerkt hat, im System eines Dritten, und dieses Deployment kann nur um Vergessen bitten, solange es das Credential der Organisation hat. MemoryService.forget_person tut das für eine Person, die noch Mitglied ist; die Notizen eines gelöschten Kontos in einem externen Speicher gehören dem Betreiber.

Backups. Eine Löschung entfernt Zeilen aus der laufenden Datenbank. Der Backup-Zeitplan eines Deployments behält sie bis zu seiner Rotation, und keine Löschung auf Anwendungsebene ändert daran etwas.

Die eigene Aufbewahrung eines Modellanbieters. Was gesendet wurde, um eine Runde zu beantworten, unterliegt der Richtlinie des Anbieters, die Modelle behandelt und dieses Deployment nicht kontrolliert.

Selbstbedienung, und was eine Administratorin hinzufügt

Für die gewöhnlichen Fälle braucht eine Person keine Administratorin. GET /me/data/export ist die ganze obige Tabelle in einem JSON-Dokument, stündlich limitiert und in der Audit-Spur festgehalten — auch wenn jemand sich selbst exportiert, denn ein Export hat die Form eines Lecks, wenn der Aufrufer nicht der ist, für den er sich ausgibt.

Es wird als Ganzes aufgebaut und serialisiert und ist deshalb begrenzt: PERSONAL_DATA_EXPORT_MAX_CHARS (standardmäßig 16 Millionen Zeichen) ist, wie viel Gesprächstext ein Dokument tragen darf; ein Konto mit mehr wird mit beiden Zahlen abgewiesen statt mit einem stillschweigend unvollständigen Dokument beantwortet. Dieses zu erzeugen, ist Sache der Betreiberin, direkt aus der Datenbank. DELETE /conversations/{id} entfernt einen Thread, seine Runden und die Dateien, die mit ihnen kamen, geprüft gegen den eigenen Besitz des Aufrufers (FA-015).

Eine Administratorin fügt zwei Dinge hinzu, beide protokolliert: GET /admin/users/{id}/export, das eine Begründung verlangt — die gesamte Gesprächshistorie einer Kollegin zu lesen ist etwa zweimal im Jahr legitim und jedes Mal ernst — und DELETE /admin/users/{id}, wo eine Begründung optional ist, weil ein Offboarding meist nichts zu erklären hat.

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
Single Sign-on gegen den eigenen Identitätsanbieter des Deployments Generisches OIDC per Discovery — Authorization Code mit PKCE, email_verified erforderlich, das Konto an sub gebunden (app/core/oauth.py, app/api/routes/v1/oauth.py). Entra ID, Okta, Keycloak; konfiguriert unter Single Sign-on test_oidc_sign_in.py
Die Registrierungsrichtlinie sichert SSO wie das Formular check_may_register innerhalb von get_or_create_oauth_user — invite_only und die Domain-Erlaubnisliste weisen auch eine Anbieter-Anmeldung ab (app/services/user.py) test_oidc_sign_in.py::TestTheRoundTrip, test_signup_policy.py
Anmeldung mit einem Verzeichniskonto Der übliche zweistufige LDAP-Bind - Suche als Dienstkonto, dann Bind als das Konto mit dem eingegebenen Passwort; ein leeres Passwort erreicht nie einen Bind, der Benutzername wird maskiert in den Filter gesetzt, zwei passende Konten sind eine Abweisung, TLS wird geprüft, und ldap:// ohne StartTLS wird beim Start abgewiesen (app/services/directory/ldap_directory.py). Siehe Verzeichnisanmeldung test_ldap_directory.py, test_directory_settings.py
Integrierte Windows-Anmeldung SPNEGO mit der Keytab dieses Dienstes; nur ein in einem Schritt angenommenes Ticket wird angenommen, und sein Principal wird über das Verzeichnis zu demselben Konto aufgelöst, das das Passwort erreicht (app/services/directory/kerberos.py, app/api/routes/v1/directory_auth.py) test_kerberos_acceptor.py, test_directory_auth_routes.py
Verzeichnisgruppen entscheiden über Mitgliedschaften Zuordnungen von Verzeichnisgruppen, angewandt bei jeder Anmeldung über LDAP, Kerberos oder OIDC. Die Synchronisierung schreibt nur Mitgliedschaften um, die sie selbst angelegt hat, stuft einen Owner nie herab und entfernt ihn nie, tritt nie einer persönlichen Organisation bei und kann keine Rolle zuordnen, die ihr Autor nicht vergeben könnte; ein Gruppen-Overage von Entra ID wird abgewiesen (app/services/directory/sync.py, app/services/directory/mappings.py, groups_claim in app/core/oauth.py) tests/integration/test_directory_groups.py, test_oidc_groups.py
Grants an Gruppen Ein Grant hat genau ein Subjekt, eine Person oder eine Gruppe (ck_resource_grant_one_subject); ein Gruppen-Grant erreicht, wer in dem Moment in der Gruppe ist, in dem der Zugriff geprüft wird, und nur über Gruppen derselben Organisation (app/repositories/resource_grant.py) tests/integration/test_directory_groups.py, test_resource_grant_repo.py
SAML, SCIM, eine erneute Prüfung des Verzeichnisses zwischen Anmeldungen Noch nicht — die offene Session eines im Verzeichnis deaktivierten Kontos hält, bis ihr Refresh Token abläuft, sofern ein Administrator das Konto nicht deaktiviert. Siehe Was dies noch nicht tut —
Rate-Limiting beim Login enforce_auth_limit (app/api/deps.py) test_auth_rate_limit.py
Eine geänderte E-Mail-Adresse wird nachgewiesen, bevor Post ihr folgt PATCH /users/me legt die Adresse in users.pending_email ab und schickt einen einmaligen Link mit einer Stunde Gültigkeit dorthin; bis der Link zurückkommt, erhält das Konto alles weiter unter seiner bisherigen Adresse, und diese wird darüber informiert, dass eine Änderung verlangt wurde. Der Link trägt die Credential-Version des Kontos, sodass ein Ändern oder Zurücksetzen des Passworts — wozu genau dieser Hinweis auffordert — ihn entwertet, und eine von einer Administratorin reparierte Adresse löscht die Vormerkung. Eine erneute Anfrage nach der bereits vorgemerkten Adresse verschickt nichts, und die Zahl unterschiedlicher Adressen pro Konto und Stunde ist begrenzt. Anfrage und Bestätigung werden beide auditiert (app/services/user.py, POST /auth/email-change/confirm) test_email_change.py
Ein wiedergespielter Refresh-Token beendet seine Kette und wird protokolliert Die Rotation behält den ersetzten Hash; ein Refresh, der dazu passt, ist der Reuse-Fall aus RFC 6819 §5.2.2.3 und schließt diese Session mit einem Audit-Eintrag (SessionService.detect_refresh_reuse). Ein Token, der innerhalb von REFRESH_REUSE_GRACE_SECONDS (standardmäßig 60) nach seiner Rotation vorgelegt wird, ist eine verlorene Antwort oder eine Anfrage aus einer Serie mit demselben Cookie und kein Replay, und er bekommt den Nachfolger, den die Zeile bereits hält, aus dem verbrauchten Token neu gebildet, sodass jede Anfrage der Serie denselben Token erhält (SessionService.claim_refresh_grace, reissue_within_grace); dieses Fenster ist der bewusst in Kauf genommene Preis test_session_revocation.py::TestReusingASpentRefreshToken, TestTheReuseGraceWindow
API-Schlüssel der Organisation Die Befugnisse eines Mitglieds, eingeschränkt auf die Berechtigungen des Schlüssels und bei jeder Anfrage aus der Mitgliedschaft des Ausstellers neu gelesen; im Ruhezustand SHA-256, einmal angezeigt, optional ablaufend, widerrufbar, pro Schlüssel begrenzt, nur auf öffentlichen Routen angenommen und in den Audit-Einträgen einer Anfrage per Präfix benannt (app/services/api_key.py, app/api/public_api.py) tests/integration/test_api_keys.py, test_api_key_service.py
OAuth 2.1 für MCP-Clients Nur öffentliche Clients, PKCE (S256) Pflicht, Redirect-URI exakt verglichen; die Zustimmung in der Konsole lehnt eine Impersonation-Sitzung und Berechtigungen ab, die das Mitglied nicht hat; Codes sind einmalig, Refresh-Tokens rotieren, und eine Wiederverwendung widerruft die Freigabe mit jedem ausgestellten Token; ein Token wird in einem bedingten Update verbraucht, sodass zwei gleichzeitige Austausche nicht beide ausstellen; die offene Registrierung ist pro Adresse begrenzt und unbenutzte Clients werden nach einem Tag entfernt (app/services/oauth_server.py, app/services/platform_mcp/_oauth.py) tests/integration/test_mcp_oauth.py
Live-Änderungsereignisse Erst veröffentlicht, nachdem ein Schreibvorgang über die öffentliche API erfolgreich war und festgeschrieben ist; der Zugriff jedes Abonnenten wird bei jedem Ereignis neu gelesen, sodass ihn eine Zeile, die er nicht lesen darf, die Änderung einer anderen Organisation oder eine Löschung einer Art, die seine Rolle nicht vollständig sieht, nie erreicht, und ein Socket, dessen Sitzung, Schlüssel oder Mitgliedschaft geendet hat, wird geschlossen (app/services/change_feed.py) tests/integration/test_change_feed.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)
Eine Audit-Frist, die eine Organisation verlängern und nie verkürzen kann Eine deploymentweite Untergrenze (standardmäßig sechs Jahre, HIPAA §164.316(b)(2)); eine kürzere Frist wird abgelehnt statt angehoben. Der Sweep löscht keine Audit-Einträge - die Hash-Kette und ihr Checkpoint stehen darauf, dass Einträge bleiben, ein Eintrag lässt sich also nicht ausmustern, ohne den Nachweis zu brechen, dass die übrigen unversehrt sind (app/core/retention.py). Siehe Aufbewahrung test_retention.py::TestWhichNumberWins, ::test_audit_resolves_to_a_period_and_is_still_not_swept
Eine Person kann ihre eigenen Daten auslesen und entfernen GET /me/data/export (stündlich limitiert, auch bei der eigenen Anfrage protokolliert) und DELETE /conversations/{id} im Rahmen des eigenen Besitzes; der Export einer Administratorin verlangt eine Begründung (app/services/personal_data.py) test_personal_data.py
Manipulationsnachweis (eine Hash-Kette) Jeder Eintrag reiht sich in die Hash-Kette seiner Organisation ein, und jede Kette trägt einen Checkpoint an ihrem Höchststand, sodass ein umgeschriebener Eintrag, ein abgeschnittenes Ende und eine gelöschte Kette je für sich erkennbar sind; agenticos cmd audit-verify läuft sie ab und endet bei einem Bruch mit einem Exit-Code ungleich null (app/core/audit.py, app/commands/audit_verify.py). Erkennung, nicht Verhinderung: wer die Zugangsdaten der Datenbank selbst hält, kann eine Kette neu schmieden oder den Trigger des Checkpoints entfernen test_audit_record.py, test_audit_chain_backfill.py, test_audit_verify_command.py, test_audit_checkpoint_migration.py

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
Eine Freigabe in einem Sprachanruf führt genau die gezeigten Argumente aus, einmal, und nur der Anrufer entscheidet sie Die Bridge des Anrufs schreibt die Zeile, zeigt die exakten Argumente, wartet 60 s und gewährt über die ID des Tool-Aufrufs (app/services/voice/approvals.py); die Route der Queue weist die Zeilen eines laufenden Anrufs ab test_voice_approvals.py::test_an_approval_grants_exactly_the_shown_arguments_once, tests/integration/test_voice_approvals_queue.py
Ein Sprachanruf endet, wenn der Zugriff wegfällt, und wird bei jedem Ausgang aufgelegt Jeder 15-Sekunden-Heartbeat prüft erneut die Sitzung, die Mitgliedschaft, den veröffentlichten Zustand des Agents und den Schalter der Organisation; jeder Ausgang durchläuft einen abgeschirmten Teardown, der das Ende des Anrufs festhält und dann den Anruf beim Provider auflegt; ein Sweep jede Minute und beim Start legt Anrufe auf, deren Heartbeat veraltet ist (app/services/voice/session.py, app/services/voice/reaper.py) test_voice_session.py::test_a_call_stops_being_allowed_when_access_goes, ::test_every_exit_path_hangs_up_the_provider_call, test_voice_reaper.py::test_a_call_whose_heartbeat_stopped_is_hung_up_and_its_run_closed
Statische Analyse erreicht die Änderung, die den Fund einführt CodeQL (security-extended) bei jedem Pull Request für Python, JavaScript/TypeScript, Rust und die Workflows, dazu ein wöchentlicher vollständiger Lauf (.github/workflows/codeql.yml). Den Merge verweigert der Merge-Schutz durch Code Scanning im Ruleset von main, nicht der Status des Jobs selbst — siehe Branches test_codeql_workflow.py
Eine bekannt verwundbare Abhängigkeit lässt den Pull Request scheitern make audit über backend/uv.lock und make audit-frontend über frontend/bun.lock, beide im Job Security Scan und in make check test_ci_parity.py
Was ein Release enthält, ist lesbar ohne es zu bauen Ein CycloneDX-SBOM pro Image, erzeugt aus dem veröffentlichten Manifest und an das Release angehängt; das Komponenteninventar ist der lesbare Index test_images_workflow.py::TestTheReleaseCarriesAnInventory

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)
Signierte Links in Access-Logs Der Redaktionsfilter deckt neben den Logs der Anwendung auch die Access- und Error-Logs von uvicorn ab (app/core/logging.py), sodass ein signiertes ?token= - das eines Embed-Sockets oder eines Links zu generierten Medien - dort geschwärzt geschrieben wird; das mitgelieferte nginx/nginx.conf protokolliert Pfade ohne Query-String; das Access-Log von Traefik (traefik/traefik.yml), eines anderen Reverse-Proxys oder des Frontend-Servers liegt außerhalb der Anwendung, hält diese Query-Tokens im Klartext fest und sollte wie ein Log aufbewahrt und weitergegeben werden, das gültige, kurzlebige Zugangsdaten enthält test_logging.py::test_a_signed_link_in_the_access_log_is_redacted
Kein Credential des Providers erreicht den Browser eines Sprachanrufs Der Server beantwortet das SDP-Angebot und hält die Sideband-Verbindung; ein Angebot mit einem Datenkanal wird abgelehnt; Client-Secrets werden nie ausgestellt; der Schlüssel wird für einen Anruf entsiegelt, und für ein Auflegen nur über OrganizationSecretService.voice_call_key, dessen Klartext an den Auflege-Endpunkt des Providers geht und nirgendwohin sonst (app/services/voice/transport.py, app/services/voice/hangup.py) test_voice_session.py::test_no_frame_carries_the_provider_key, test_voice_transport.py::test_an_offer_with_a_data_channel_is_refused
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), deren script-src eine Nonce pro Request und 'strict-dynamic' trägt statt 'unsafe-inline' test_security_headers.py, inkl. test_an_excluded_path_keeps_its_framing_but_drops_the_csp; csp.test.ts, middleware.test.ts
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
Von Agents verfasste Seiten können die Konsole nicht erreichen Eine veröffentlichte App wird von einer Route ohne Cookies hinter einem kurzlebigen signierten Token ausgeliefert, unter Content-Security-Policy: sandbox ohne allow-same-origin — ein opaker Origin — und ohne allow-popups, plus connect-src 'none', keine entfernte Quelle außer dem eigenen Bibliothekspfad des Deployments und frame-ancestors, das nur die Konsole nennt (und für eine einbettbare öffentliche Seite das Einbettungsdokument und die gelisteten Sites); ein Link in der Seite öffnet erst, nachdem eine Person seine Adresse bestätigt hat, und eine Folgenachricht, um die sie bittet, wird erst gesendet, nachdem eine Person sie gelesen und auf Senden geklickt hat; die eine Ausnahme bei Bildern ist der eigene Kachelpfad des Deployments, der drei geprüfte Zahlen nimmt und die Kachel vom Server holt, den VISUAL_MAP_TILES nennt, sodass eine Seite diesen Server nie erreicht; optional von einer eigenen registrierbaren Domain (ARTIFACT_ORIGIN) test_artifact_routes.py::TestTheContentRoute, test_artifact_service.py::TestThePolicy, test_artifact_pages.py, test_map_tiles.py, test_tile_proxy.py, artifacts.test.tsx, visual-frame.test.tsx, visual-panel.test.tsx, csp.test.ts
Rate-Limits auf öffentlichen Oberflächen Redis-gestützte Limits auf der Run-API, dem Embed-Widget, den gehosteten Seiten, öffentlichen App-Links und deren signierten Inhaltsadressen sowie den signierten Links zu generierten Medien, die ein Besucher einer gehosteten Seite erhält, gezählt pro Link (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
Was Anrufe über einen geleakten Embed-Link kosten können, ist begrenzt Der Start von Sprachanrufen ist pro Adresse, pro Link und pro jwt-Subject begrenzt, auf einem Embed sind höchstens VOICE_MAX_CALLS_PER_EMBED Anrufe offen, und jeder ist durch die Anrufobergrenze und das Budget begrenzt (app/services/rate_limit.py, app/services/voice/starter.py) test_rate_limit.py, test_voice_starter.py, tests/integration/test_voice_call_slot.py
Ein Diktat kostet höchstens die längste Diktatdauer Standardmäßig nur WAV mit 16 kHz, Mono und 16-Bit-PCM: streng geparst, seine Samples gezählt und über dictation_max_seconds abgelehnt, bevor der Provider aufgerufen wird, und hinter einem Header gesendet, den der Server baut (app/services/ml/pcm_wav.py, MLService.transcribe_dictation). WebM, Ogg und MP4 erst, wenn ein App-Admin dictation_compressed_audio einschaltet, hinter einem Dialog, der das Risiko benennt test_pcm_wav.py, test_ml_service.py, dictation-audio-card.integration.test.tsx

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.

Das HIPAA-Profil, und was es nicht behauptet

Eine Sicherheitsprüfung fragt nicht „ist diese Software konform“. HHS zertifiziert keine Software, und OCR erkennt keine private Zertifizierung an. Sie fragt können wir das in unserer konformen Umgebung betreiben, und lässt sich das belegen - und die Antwort ist eine mitgelieferte Konfiguration plus ein Befehl, der ein laufendes Deployment dagegen prüft (#1448).

uv run agenticos cmd doctor --profile hipaa

Eine Zeile je Kontrolle, jede mit der Einstellung, die sie erfüllt, oder der, die es nicht tut, und ein Exit ungleich null bei jedem Fehlschlag, damit es in der CI einer Kundin laufen kann. Die Konfiguration ist deploy/profiles/hipaa/: ein Compose-Overlay, das ohne die Einstellungen, die es nicht vorbelegen kann, gar nicht erst startet, und eine kommentierte Env-Datei.

Diesen Absatz im selben Atemzug mit dem Profil lesen. Es beantwortet die technischen Sicherungen, §164.312, und nur diese. Administrative Sicherungen (§164.308 - Risikoanalyse, Schulung, Sanktionsrichtlinie, Notfallplan, Business-Associate-Verträge) und physische (§164.310) gehören der Betreiberin und werden es bleiben. Ein Profil, das anderes nahelegt, wäre eine Behauptung, die niemand stützen kann.

Das Blatt

Kontrolle Sicherung Erfüllt durch
postgres-tls §164.312(e)(1) POSTGRES_SSLMODE=verify-full. require verschlüsselt und prüft kein Zertifikat, das Profil nimmt es deshalb nicht
redis-tls §164.312(e)(1) REDIS_SSL=true. Beide Stores müssen Ihre eigenen sein: die mitgelieferten db und redis haben keinen TLS-Listener, also startet das Overlay ohne POSTGRES_HOST und REDIS_HOST nicht
browser-tls §164.312(e)(1) FRONTEND_URL und PUBLIC_BASE_URL über https, terminiert von Ihrem Reverse Proxy. Attestiert, und eine http-Adresse wird hier abgelehnt: jede andere Kontrolle kann bestehen, während eine Anmeldung im Klartext über die Client-Grenze geht
vault-key §164.312(a)(2)(iv) Ein Vault-Masterschlüssel von mindestens 64 Zeichen. HKDF leitet aus allem einen richtig dimensionierten Wrapping-Key ab und kann einem ratbaren Geheimnis keine Entropie hinzufügen
content-at-rest §164.312(a)(2)(iv) Der Betreiberin. Postgres-Daten, das Medien-Volume und das Sandbox-Workspace-Verzeichnis verschlüsselt ein Volume oder eine Platte, nicht diese Anwendung
local-model §164.312(e)(1) Jedes Modellprofil aus dem eigenen Netz. Geparst wird der Hostname - eine private Adresse, localhost, ein bloßes ollama/litellm/vllm oder ein .internal/.local/.svc-Name - https://ollama.vendor.example ist also nicht lokal, und eines ohne base_url ist per Definition die öffentliche API des Anbieters
traces-local §164.312(e)(1) Nicht gesetztes LOGFIRE_TOKEN und kein veröffentlichter Agent und keine benannte Umgebung mit eigenem Tracing-Token - jedes hängt einen eigenen Exporter an, und observability.content ist standardmäßig full
sso §164.312(d) OIDC_ISSUER, allein per Discovery konfiguriert - der Issuer ist die einzige URL, und die Endpunkte für Autorisierung, Token und JWKS kommen aus dessen .well-known/openid-configuration - oder KERBEROS_ENABLED, die Anmeldung mit dem eigenen Ticket der Domäne. Ist keines von beiden gesetzt, ist die Kontrolle unerfüllt, und das Blatt sagt es: Mit LDAP_URL allein sagt es, dass die Passwörter die des Verzeichnisses sind, aber hier ohne Single Sign-on eingegeben werden, und sonst, dass sich Menschen mit Passwörtern anmelden, die dieses Deployment speichert. Multi-Faktor-Authentifizierung gehört dem Identity Provider, und das Blatt beansprucht sie in keine Richtung
signup §164.312(a)(1) invite_only oder closed
audit-retention §164.312(b) Eine Audit-Untergrenze von mindestens 2190 Tagen - die sechs Jahre aus §164.316(b)(2)
audit-chain §164.312(c)(1) Die Hash-Kette und ihr Checkpoint. Erkennung, nicht Verhinderung - siehe Audit-Kontrollen

Drei Ergebnisse, und das mittlere wiegt. ok und !! sind die Antworten dieses Codes. -- ist eine Kontrolle, die wirklich der Betreiberin gehört, benannt statt still bestanden - ein Blatt, das überginge, was es nicht sehen kann, läse sich als vollständig und wäre es nicht - und sie lässt den Befehl nicht fehlschlagen, denn eine Kontrolle, die von hier niemand belegen kann, ist eine, die nie jemand bestehen könnte.

Wer die Business Associate ist

Eine Kundin, die das auf eigener Infrastruktur betreibt, bekommt Software. Niemand hier fasst ihre PHI an, und es braucht keinen Vertrag. Ein Deployment, das jemand anders für sie betreibt, macht diesen jemand zur Business Associate - und das ist ein Vertrag, kein Konfigurationsschalter.

Warum das Profil auf ein lokales Modell setzt

Ein gehostetes Modell nimmt den Inhalt jedes Laufs mit, seine Nutzung bedeutet also einen Vertrag mit diesem Anbieter - und diese Verträge sind enger, als man erwartet. Eine HIPAA-fähige Organisation bei einem großen Anbieter schließt typischerweise Codeausführung und Web-Abrufe aus, also genau die Form der Capabilities sandbox, code_execution und web_fetch. Wer so einen Vertrag unterschreibt und dann einen Agenten darauf baut, erfährt es während eines Vorfalls.

Lokale Inferenz nimmt die Frage weg, und deshalb ist sie die Vorgabe des Profils und kein Vorschlag.

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 — das optional ist und, sobald ein deploymentweites Token gesetzt ist, jeden Run traced, den der API-Prozess bedient, mit dem Inhalt jedes Agenten, der nicht none verlangt hat.
  • Konnektor- und API-Credentials sind pro Organisation im einen Vault versiegelt; kurzlebige Bearer-Token und der übrige Inhalt im Ruhezustand (Nachrichten, RAG, Sandboxes) sind es nicht. Hochgeladene Dateien sind das, was sich bewegt hat: FILE_STORAGE_BACKEND=s3 legt sie in einen Objektspeicher, der jeden Schreibvorgang verschlüsselt.
  • Jede Kontrolle in der Matrix benennt einen Mechanismus und einen Test — und im selben Atemzug ihre Lücken: der Manipulationsnachweis verlinkt das Issue, das ihn 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.