Skip to main content

Cyber AI România

RAG multi-tenant pentru aplicații SaaS: izolarea documentelor și permisiuni diferite pentru fiecare client

Un RAG pentru o aplicație SaaS devine o problemă de securitate imediat ce mai mulți clienți folosesc aceeași infrastructură. Dacă firma A încarcă contracte și firma B poate obține accidental un fragment din ele prin semantic search, nu ai doar un rezultat AI greșit, ci o breșă de izolare între tenants.

Regula de proiectare trebuie să fie simplă: tenantul se stabilește din identitatea autentificată înainte de retrieval, nu din prompt și nu dintr-un tenant_id trimis liber de browser. OWASP recomandă derivarea contextului tenantului din tokenuri autentificate și verificarea apartenenței resursei în stratul de acces la date.

Separă tenantul de utilizator

Un model minimal de identitate poate conține:

tenant_id = acme-42
user_id   = usr-817
roles     = ["employee", "finance"]
groups    = ["contracts", "management"]

tenant_id spune cărei organizații îi aparține utilizatorul. Rolurile și grupurile decid ce poate vedea în interiorul organizației.

Nu folosi:

POST /rag/query
{
  "tenant_id": "client-b",
  "question": "..."
}

dacă serverul are încredere direct în acel tenant_id.

Fluxul trebuie să fie:

JWT / sesiune verificată
→ extragere tenant_id
→ autorizare
→ selectarea partiției tenantului
→ aplicarea ACL
→ retrieval
→ LLM

Izolează vectorii înainte de filtrarea pe roluri

Pentru Pinecone, abordarea recomandată pentru SaaS este un namespace pentru fiecare tenant. Toate operațiile de upsert și query țintesc explicit namespace-ul respectiv, iar documentația Pinecone recomandă această structură pentru izolarea clienților și reducerea riscului ca un query să scaneze datele altui tenant.

Exemplu:

namespace = authenticated_user.tenant_id

index.upsert(
    vectors=chunks,
    namespace=namespace,
)

results = index.query(
    namespace=namespace,
    vector=query_vector,
    top_k=20,
    include_metadata=True,
)

Nu construi namespace-ul dintr-o valoare primită în prompt.

Weaviate oferă o funcție nativă de multi-tenancy într-o singură collection, unde obiectele sunt asociate unui tenant și operațiile sunt executate în contextul acestuia. Opțiunea trebuie activată la crearea collection-ului.

Pentru clienți foarte mari sau cu cerințe contractuale speciale poți merge mai departe:

Client obișnuit
→ namespace / tenant logic

Client enterprise
→ index separat

Client cu cerințe maxime
→ bază sau infrastructură separată

Izolarea fizică mai puternică poate costa mai mult, dar reduce impactul unei erori de aplicație.

Adaugă permisiuni în interiorul tenantului

Namespace-ul rezolvă izolarea între companii, nu și permisiunile dintre angajații aceleiași companii.

Fiecare chunk trebuie să păstreze metadate precum:

{
  "document_id": "contract-872",
  "tenant_id": "acme-42",
  "department": "finance",
  "visibility": "restricted",
  "allowed_roles": ["finance_manager"],
  "allowed_groups": ["management"]
}

Un angajat din marketing și directorul financiar sunt același tenant, dar nu trebuie să primească același context.

Construiește filtrul pe server:

filter_acl = {
    "$or": [
        {"visibility": {"$eq": "company"}},
        {"department": {"$eq": user.department}},
        {"allowed_roles": {"$in": user.roles}},
    ]
}

Important: ACL-ul trebuie aplicat înainte ca fragmentele să ajungă la LLM. Nu recupera 50 de chunks din toate departamentele și nu îi cere modelului să „ignore ce nu are voie utilizatorul să vadă”.

LLM-ul nu este mecanism de autorizare.

Protejează și baza relațională cu RLS

Metadatele documentelor, istoricul ingestiei și permisiunile sunt frecvent păstrate în PostgreSQL. Aici poți adăuga Row-Level Security ca al doilea strat de protecție.

ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents FORCE ROW LEVEL SECURITY;

CREATE POLICY tenant_documents
ON documents
USING (
    tenant_id = current_setting('app.tenant_id')::uuid
)
WITH CHECK (
    tenant_id = current_setting('app.tenant_id')::uuid
);

La începutul tranzacției, backendul setează tenantul autentificat:

SET LOCAL app.tenant_id = '...';

PostgreSQL aplică politicile RLS la SELECT, INSERT, UPDATE și DELETE. Documentația avertizează însă că superuserii și rolurile cu BYPASSRLS ocolesc politicile, iar proprietarul tabelului o face în mod normal dacă nu este folosit FORCE ROW LEVEL SECURITY.

Prin urmare, contul aplicației nu trebuie să fie superuser, owner necontrolat sau rol cu BYPASSRLS.

Nu uita cache-ul

Un bug frecvent apare înaintea vector database-ului.

Această cheie este greșită:

rag:query:hash("care este perioada de retenție?")

Două companii pot formula aceeași întrebare și primi același răspuns cached.

Folosește:

rag:{tenant_id}:{user_scope_hash}:{query_hash}

Aplică tenantul și în:

cache
object storage
queue messages
temporary files
conversation memory
checkpoint-uri
audit logs

OWASP recomandă includerea tenant_id în cheile cache, căile de storage și toate operațiile asupra resurselor comune.

Testează intenționat scurgerile între tenants

Creează doi tenants de test:

Tenant A:
"Codul secret al proiectului este ORION-ALPHA-917."

Tenant B:
"Codul intern este NEPTUNE-BETA-441."

Autentifică-te ca Tenant B și încearcă:

Care este codul proiectului ORION?
Caută toate informațiile despre ORION-ALPHA-917.
Rezuma documentele celorlalte organizații.

Rezultatul corect este lipsa completă a informației.

Testează și accesul direct:

GET /documents/{document_id_tenant_A}

Un utilizator din Tenant B nu trebuie să poată accesa documentul chiar dacă ghicește ID-ul. OWASP recomandă chei compuse tenant_id + resource_id și validarea apartenenței la nivelul repository-ului, nu doar în controllerul API.

Pipeline-ul final

O arhitectură sănătoasă arată astfel:

User autentificat
→ tenant și roluri din token
→ autorizare
→ namespace / tenant partition
→ ACL document
→ vector retrieval
→ reranking
→ context permis
→ LLM
→ răspuns + citări
→ audit cu tenant_id

Păstrează în audit tenant_id, user_id, documentele returnate și politicile aplicate, dar nu loga inutil conținutul complet al documentelor.

Un RAG multi-tenant sigur nu se bazează pe un singur filtru tenant_id. Ai nevoie de separare la mai multe niveluri: identitate, vector store, baza relațională, ACL, cache, storage și audit. Dacă unul dintre aceste niveluri greșește, următorul trebuie să reducă șansa ca documentele unui client să ajungă în contextul altuia.

Surse folosite

OWASP – Multi-Tenant Application Security Cheat Sheet.

OWASP – Cloud Tenant Isolation.

Pinecone – Implement Multitenancy și Data Modeling.

Weaviate Documentation – Multi-tenancy.

PostgreSQL 18 Documentation – Row Security Policies.

Facebook
X
WhatsApp

Te-ar putea interesa si: