Przejdź do treści

Bezpieczeństwo

Ta strona jest tym, co przegląd bezpieczeństwa w kształcie HIPAA albo SOC 2 dostaje do ręki: granice zaufania, jakie dane opuszczają wdrożenie i do kogo, co jest gdzie szyfrowane, oraz macierz kontroli, która dla każdej kontroli nazywa mechanizm w tym kodzie, który ją realizuje, i test, który trzyma ją w mocy.

Opisuje to, co jest, a nie to, co byłoby miłe. Wiersz bez mechanizmu mówi to wprost i linkuje issue, które by go zbudowało. Jak zgłosić podatność i jaka jest produkcyjna lista kontrolna hardeningu — zobacz SECURITY.md w korzeniu repozytorium; ta strona to cała reszta, w jednej kopii.

Dwie sąsiednie strony odpowiadają na pytania, które przegląd zadaje zaraz potem, i nie są tu powtarzane: Ochrona danych — gdzie leżą dane osobowe, co faktycznie obejmuje usunięcie i które luki są wciąż otwarte, oraz Licencje — każdy komponent trzeciej strony, który wiozą obrazy.

Model zagrożeń

Platforma jest self-hostowana i wielotenantowa. Założeniem projektowym jest, że infrastruktura operatora jest zaufana, a każde żądanie do niej nie jest — więc granicami, które mają znaczenie, są te, które żądanie przekracza w drodze do danych.

Granica Co ją przekracza Zaufana po drugiej stronie?
Przeglądarka → BFF (route handlery Next.js) Ciasteczko sesji, nagłówek organizacji, dane z formularza Nie — ale BFF nie weryfikuje ciasteczka: czyta httpOnly access_token i przekazuje go jako nagłówek bearer (frontend/src/lib/platform-proxy.ts). To granica przekazywania poświadczenia; weryfikacja jest zadaniem API
BFF → API (FastAPI) JWT związany z sesją w bazie, nagłówek X-Organization-Id Nie — token jest weryfikowany przy każdym żądaniu, sesja sprawdzana pod kątem unieważnienia, a organizacja rozstrzygana z tokena
API → PostgreSQL / Redis Zapytania i odczyty z cache, po TLS, gdy jest skonfigurowany Tak — magazyn należy do operatora; co chroni w spoczynku, jest pod „Co jest gdzie szyfrowane”
API / worker → providerzy modeli, kanały, serwery MCP, dostawcy wyszukiwania, Logfire Prompty, wywołania narzędzi, zapytania, odpowiedzi, trace'y Nie — to strony trzecie; co do nich trafia, jest decyzją per agent, z wyjątkiem tracingu na poziomie całego wdrożenia (poniżej)
Worker → konektory (Google Drive, S3, …) Poświadczenia odpieczętowane z vaultu, pobrane dokumenty Nie — poświadczenie konektora to sekret w vault wskazywany po id

Władza wewnątrz tenanta nigdy nie jest nazwą roli na route'cie: to wiersz członkostwa plus katalog uprawnień (app/core/permissions.py), rozstrzygany per zasób. Dwóch wywołujących z tą samą rolą może sięgnąć do różnych wierszy, bo grant na jednym zasobie poszerza to, na co rola pozwala, nie awansując członka.

Co opuszcza wdrożenie

Nic nie dzwoni do domu. Każde wywołanie na zewnątrz jest tym, które wdrożenie skonfigurowało, i każde jest granicą, o którą przegląd u klienta zapyta.

Do Co Kiedy
Skonfigurowany provider modelu Prompt, wyjście modelu, argumenty i wyniki narzędzi Każdy run — chyba że model działa na własnej infrastrukturze operatora, wtedy nic nie wychodzi
Skonfigurowany kanał (Slack, Telegram, Mattermost) Wygenerowane odpowiedzi agenta — tekst, obrazy i załączniki Zawsze, gdy agent jest wystawiony przez ten kanał; każde send_message publikuje u providera (app/services/channels/)
Logfire Trace'y, które domyślnie niosą prompty i wyjścia Dwie niezależne ścieżki. Token observability per agent trace'uje tego agenta; LOGFIRE_TOKEN na poziomie wdrożenia instrumentuje każdy run globalnie (app/core/logfire_setup.py), więc przy nim ustawionym treść runów wychodzi niezależnie od jakiegokolwiek ustawienia per agent. Tryb content tego tokena decyduje, ile niesie span - none sprowadza go do czasu, tokenów, kosztu i nazw narzędzi (#1413). Stan pośredni z filtrem to #1616
Serwery MCP Wywołania narzędzi i ich argumenty Tylko dla narzędzi, do których agent jest podpięty
Dostawca web search (Tavily, DuckDuckGo) Zapytanie wyszukiwania Tylko gdy przyznana jest capability wyszukiwania
Provider embeddingów Tekst dokumentu, przy ingest Tylko dla bazy wiedzy, której provider jest zdalny

Co jest gdzie szyfrowane

Jest jeden mechanizm szyfrowania na poziomie aplikacji i celowo jest jedyny: vault (app/core/vault.py). Każde poświadczenie konektora i API w spoczynku jest zapieczętowane w kopercie per właściciel, której klucz opakowujący jest wyprowadzany przez HKDF z organizacji (albo użytkownika), do której należy — więc szyfrogram skopiowany do wiersza innej organizacji nie da się odpieczętować. Klucz główny jest rotowalny bez ponownego szyfrowania ładunków.

Nie wszystko, co platforma przechowuje, jest poświadczeniem w vault, i mówimy to wprost, bo przegląd i tak to znajdzie:

  • Krótkożyjące tokeny bearer — zaproszenia do organizacji (OrganizationInvitation.token), żądania połączenia kanału (ChannelLinkRequest.token) i linki do udostępnionych konwersacji (ConversationShare.share_token) — to losowe kolumny String(64) wyszukiwane po równości, nie pieczętowane w vault. Kto ma wartość, ten może jej użyć, więc chroni je wygaśnięcie i jednorazowość, a nie szyfrowanie. Wyjątkiem są refresh tokeny sesji, haszowane w spoczynku (sessions.refresh_token_hash).
  • Wgrane pliki i pliki z czatu leżą na systemie plików kontenera API otwartym tekstem (app/services/file_storage.py) — chroni je wyłącznie szyfrowanie wolumenu.
  • Treści wiadomości, rag_documents i ich wektory oraz workspace'y sandboksa są przechowywane jako kolumny z tekstem jawnym, wiersze pgvector i pliki workspace'u. Vault pieczętuje poświadczenia, nie treść; ochrona tych rzeczy w spoczynku jest na poziomie dysku.

Backend plików zgodny z S3, z szyfrowaniem po stronie serwera, jest odpowiedzią na poziomie aplikacji dla object storage i jest śledzony w #1423.

Macierz kontroli

Jeden wiersz na kontrolę, mechanizm, który ją realizuje, i test, który trzyma ją w mocy. Ujęte względem zabezpieczeń technicznych HIPAA §164.312 i SOC 2 CC6–CC8.

Kontrola dostępu · HIPAA §164.312(a) · SOC 2 CC6

Kontrola Mechanizm Trzymane przez
Izolacja tenantów, nawet gdy wywołujący jest właścicielem wiersza resolve_access odmawia zasobowi, którego organization_id się różni, zanim sprawdzi własność (app/services/access.py) test_resource_access.py::TestTenantBoundary, test_conversation_tenant_isolation.py, test_platform_flows.py
Uprawnienie na każdym route'cie kolekcji Zależność route'u require(*perms) na listowaniu, tworzeniu i route'ach katalogu (app/api/deps.py), katalog w app/core/permissions.py test_platform_routes.py::TestEachRouteDemandsItsOwnPermission
Route'y per zasób autoryzują w serwisie, nie na route'cie Route działający na jednym agencie, skillu albo kolekcji nie niesie bramki require() — bramka roli odmówiłaby posiadaczowi grantu, zanim grant by zadziałał — i zamiast tego woła resolve_access (app/services/access.py) test_platform_routes.py::TestEveryPlatformRouteIsGuarded (każdy route jest bramkowany albo rozstrzygany w serwisie)
Grant poszerza dostęp, nie awansując członka resolve_access per zasób bierze max(zakres roli, grant) (app/services/access.py) test_resource_access.py::TestGrantsWidenAccess, ::TestPermissionsGrantsCannotWiden
Wzmianka w kanale uruchamia run jako nadawca, nie jako bot Używany jest własny AuthContext powiązanego, aktywnego nadawcy (app/services/channels/mentions.py) test_channel_mentions.py::TestAnswer::test_the_run_carries_the_senders_own_role

Uwierzytelnianie · HIPAA §164.312(d) · SOC 2 CC6

Kontrola Mechanizm Trzymane przez
JWT (HS256), hasła przez bcrypt app/core/security.pyverify_token, get_password_hash test_security.py, test_auth.py
Klucze API porównywane w stałym czasie secrets.compare_digest (app/api/deps.py) test_auth.py, sprawdzenia HMAC webhooków w adapterach kanałów
Sesje w bazie danych z unieważnianiem Tabela sessions + SessionService; token związany z claimem sid (app/services/session.py, app/api/routes/v1/sessions.py) test_session_verify.py, test_session_revocation.py
Limitowanie prób logowania enforce_auth_limit (app/api/deps.py) test_auth_rate_limit.py

Kontrole audytowe · HIPAA §164.312(b) · SOC 2 CC7

Kontrola Mechanizm Trzymane przez
Mutacje istotne dla governance zapisywane w transakcji żądania record_audit (app/core/audit.py) w mutującym serwisie — rotacja sekretu, podpięcie skilla / synchronizacji / MCP, członkostwo, udostępnianie, zatwierdzenia, eksporty i więcej; zapisywane do app_admin_audit_logs. To nie jest pokrycie każdego zapisu (CRUD bazy wiedzy, choćby, nie jest audytowany) test_skill_binding_audit.py, test_sync_source_audit.py
Ślad jest czytelny dla audytora GET /audit, bramkowane na audit:read (app/services/audit.py) test_audit_service.py
Eksport śladu (CSV/JSONL) GET /audit/export w oknie czasu, bramkowany na audit:read, zapisujący własny odczyt w śladzie; eksporty runów, zatwierdzeń i wydatków robią to samo (#1422) test_exporting.py (eksport i jego własny wpis audytowy)
Dowód nienaruszalności (łańcuch haszy) Jeszcze nie#1622

Integralność · HIPAA §164.312(c) · SOC 2 CC8 (zarządzanie zmianą)

Kontrola Mechanizm Trzymane przez
Spec jest odrzucany przy publikacji, nigdy w czasie runu validate_spec (app/services/agent_registry.py) — nieznana capability, nieprzyznany scope, secret_id złego rodzaju albo z innej organizacji, osobiste połączenie MCP test_agent_registry.py, test_capability_secrets.py::TestPublishValidation
Budżet jest sprawdzany przed żądaniem do modelu, a koszt zapisywany nawet przy błędzie BudgetGuard.wrap_model_request bramkuje przed wywołaniem (app/agents/capabilities/budget/); koszt runu jest zapisywany w terminalnym finally (app/services/agent_runner.py) test_spend.py::TestBudgetGuard, test_agent_runner.py::…::test_a_failed_run_still_records_its_cost
Zatwierdzenie jest rozstrzygane dokładnie raz ApprovalService.decide odmawia wierszowi innemu niż oczekujący, odczytanemu for_update (app/services/approvals.py) test_approvals_queue.py::TestDecidingTwiceIsRefused

Poufność poświadczeń · HIPAA §164.312(a)(2)(iv)

Kontrola Mechanizm Trzymane przez
Żaden sekret otwartym tekstem w odpowiedzi API ani we wpisie audytowym SealedStr/CredentialStr maskują każdy repr; podpowiedzi to wyłącznie ostatnie 4 znaki (app/core/secret_kinds.py, app/core/vault.py) test_no_secret_escapes.py (przemiata całą powierzchnię OpenAPI), test_capability_secrets.py::TestInjection
Logi nie są częścią tej gwarancji Zniekształcona odpowiedź tokena MCP OAuth trafia do logów przez ValidationError Pydantica, który powtarza swoje wejście — znana luka, #1626 test_mcp_connections.py::test_an_unreadable_token_response_does_not_echo_its_input (dokumentuje, że token ląduje w caplog)
Poświadczenie jest w spoczynku związane ze swoją organizacją Koperta HKDF per właściciel (app/core/vault.py); zakres to poświadczenia konektorów i API — o tokenach bearer, których nie obejmuje, mówi „Co jest gdzie szyfrowane” test_secret_tenant_isolation.py, test_vault.py

Bezpieczeństwo transmisji · HIPAA §164.312(e) · SOC 2 CC6

Kontrola Mechanizm Trzymane przez
TLS do PostgreSQL i Redisa POSTGRES_SSLMODE, REDIS_SSL (app/core/config.py); doctor raportuje żywy stan Postgresa z pg_stat_ssl Postgres, na żywym połączeniu: test_store_tls.py; Redis, przy budowie URL-a i w doctor: test_config.py, test_doctor_sandbox.py
Nagłówki ramkowania i MIME na każdej odpowiedzi; CSP na wszystkich poza endpointami referencji API SecurityHeadersMiddleware (app/core/middleware.py), którego exclude_paths zdejmują CSP — nie ramkowanie ani MIME — dla OpenAPI, Swaggera i ReDoc; plus CSP frontendu per wdrożenie (frontend/src/middleware.ts) test_security_headers.py, w tym test_an_excluded_path_keeps_its_framing_but_drops_the_csp
HTTPS i HSTS Terminowane na reverse proxy — dołączony nginx/nginx.conf ustawia HSTS; aplikacja z założenia nie Sprawa wdrożenia; zobacz listę kontrolną hardeningu
Limity zapytań na publicznych powierzchniach Limity oparte o Redis na API runów, widgecie embed i stronach hostowanych (app/services/rate_limit.py); limity per nadawca na botach kanałów (app/services/channels/router.py) test_rate_limited_surfaces.py; limit bota kanału jest zaimplementowany, ale cienko przetestowany

Odmowy jako zbiór

Testy odmów powyżej niosą marker security. make test-security uruchamia cały zbiór, a CI publikuje zebraną listę jako artefakt security-tests.txt przy każdym przebiegu backendu (#1417) — więc odmowy da się policzyć i przeczytać, a nie tylko przyjąć na wiarę. Test, którego nazwa albo moduł wspomina tenanta, uprawnienie, budżet, zatwierdzenie, sekret albo tekst jawny, a nie ma markera, wywala tests/test_security_marker.py, co trzyma listę kompletną w miarę rozrostu suite.

Podsumowanie

  • Ufaj infrastrukturze operatora; nie ufaj żadnemu żądaniu do niej. Granicami, które mają znaczenie, są przeglądarka → BFF → API → magazyn oraz API/worker → strony trzecie. BFF przekazuje ciasteczko sesji; API jest tym miejscem, gdzie żądanie jest weryfikowane.
  • Jedyne dane, które wychodzą, to te, o których wyjściu zdecydowało wdrożenie — providerzy modeli, kanały, serwery MCP, dostawcy wyszukiwania i embeddingów oraz Logfire; token Logfire na poziomie wdrożenia trace'uje treść każdego runu.
  • Poświadczenia konektorów i API są zapieczętowane per organizacja w jednym vaulcie; krótkożyjące tokeny bearer i treść w spoczynku (pliki, wiadomości, RAG, sandboksy) nie są, a #1423 jest odpowiedzią na poziomie aplikacji dla object storage.
  • Każda kontrola w macierzy nazywa mechanizm i test, i tym samym tchem nazywa swoje luki — dowód nienaruszalności, szyfrowanie plików na poziomie aplikacji i stan pośredni dla trace'ów linkują issue, które by je zbudowały.
  • Podatności zgłaszaj i listę kontrolną hardeningu uruchamiaj z SECURITY.md; obok tej strony czytaj Ochronę danych i Licencje.