← Nazad na sve tekstove
11. јун 2026.TechRevati

Izolacija zakupaca u RAG sistemima: granicu gurnite u same skladišne mašine

Praktičarski vodič za sprovođenje granice između zakupaca u RAG platformi ispod aplikativnog sloja — PostgreSQL Row-Level Security i četiri zaobilaznice koje je tiho poništavaju, vektorske kolekcije po zakupcu i koliko zaista koštaju, test koji može da padne, i pošten spisak onoga od čega ništa od toga ne štiti.

  • arhitektura
  • bezbednost
  • rag

Zašto je WHERE tenant_id = ? filtriranje, a ne izolacija

Prvo pitanje koje svaki CISO postavi o višezakupačkoj platformi za pretragu (retrieval) jeste kako sprečavate da dokumenti jednog zakupca završe u odgovorima drugog. Uobičajeni odgovor pokazuje na aplikativni sloj: međusloj raščlani token, izvuče identifikator zakupca, a ORM svakom upitu doda uslov.

To je filtriranje. Radi tačno onoliko dugo koliko svaki upit, u svakoj putanji koda, koju napiše svaki inženjer koji će ikada dodirnuti ovu bazu koda, ne zaboravi da ponese taj uslov. Načini otkaza su svakodnevni, ne egzotični:

  • Nadogradnja biblioteke pretvori loše formiran token iz tvrde greške u upozorenje, pa se zahtev nastavlja sa praznim identifikatorom zakupca.
  • Direktan SQL upit napisan za izveštajnu krajnju tačku zaobilazi ORM sloj koji je nosio filter.
  • Složen JOIN navede ORM da ispusti uslov u podupitu.
  • Pozadinski posao — reindeksiranje, migracija, izvoz — uopšte se izvršava bez konteksta zahteva.

Ništa od toga nije loš inženjer. To je statistički ishod bezbednosnog modela koji mora ispravno da se iznova implementira u svakoj budućoj liniji koda. U RAG sistemu posledica je gora od običnog curenja podataka: iscurele vrste se ne pojavljuju samo u odgovoru, već se sklapaju u prompt, a model rezimira podatke konkurenta pravo u čitaočev radni tok. Nema očigledno pogrešnog izlaza koji bi to uhvatio.

Alternativa je da granicu pomerite ispod aplikacije, u mašine koje drže podatke — tako da greška u aplikaciji proizvede vidljiv lom umesto tihog curenja. U EinCoreRAG-u smo to uradili na dva sloja nezavisno, relacionom i vektorskom, tako da otkaz na jednom ne može da dosegne podatke drugog. Prateća epizoda podkasta (na engleskom) pokriva rezonovanje; ovaj tekst je implementacioni detalj, uključujući delove koji grizu.

Sloj 1 — PostgreSQL Row-Level Security

Mehanizam je mali. Zamke oko njega nisu.

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

CREATE POLICY tenant_isolation ON documents
  USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);

Svaka transakcija počinje postavljanjem zakupca samo za tu transakciju:

SET LOCAL app.current_tenant_id = '';

Četiri detalja u tih pet linija nose celu zamisao.

SET LOCAL, ne SET. Svako produkciono postavljanje stavlja pred Postgres objedinjivač veza (pooler), a on multipleksira mnogo klijentskih zahteva preko nekoliko dugoživećih veza. Običan SET piše u sesiju, koja nadživljava zahtev i pripada onome ko sledeći dobije tu vezu. SET LOCAL ograničava vrednost na transakciju, a Postgres je čisti pri potvrdi ili poništenju, što ostavlja vezu čistom za sledećeg korisnika. Ako koristite PgBouncer, to znači i da vam treba objedinjavanje na nivou transakcije i sav rad unutar eksplicitne transakcije — u režimu objedinjavanja po naredbi SET LOCAL i naredbe koje od njega zavise mogu da završe na različitim pozadinama.

true u current_setting. To je zastavica missing_ok. Bez nje zahtev koji je zaboravio da postavi promenljivu diže grešku; sa njom pretraga vraća NULL. Poređenje tenant_id sa NULL nikada nije tačno, pa politika ne pogađa nijednu vrstu. Upit uspeva i ne vraća ništa. To je svojstvo „fail-closed" i namerno je: glasan, prazan rezultat je bezbedan ishod, a prazan rezultat tamo gde su se očekivali podaci jeste nešto što vide i nadzor i testovi.

FORCE ROW LEVEL SECURITY. Sam ENABLE ne primenjuje politike na vlasnika tabele. Aplikacije se vrlo često povezuju kao uloga koja je vlasnik šeme, jer je tako bio podešen alat za migracije — a ta uloga čita svaku vrstu kao da RLS ne postoji. To je najčešći način da tim uvede RLS, testira ga kao superkorisnik, vidi da „radi", a u produkciji nema nikakvu izolaciju. Ili se povežite kao uloga koja nije vlasnik i ima samo DML prava, ili dodajte FORCE — ili oboje. Uloge sa atributom BYPASSRLS takođe su izuzete, a superkorisnici su uvek — pa nijedna aplikacija ne sme da se poveže kao neka od njih.

Funkcije SECURITY DEFINER su rupa koju sami iskopate. Pomoćna funkcija — obračunski agregat, omotač za pretragu — napravljena sa SECURITY DEFINER izvršava se sa privilegijama uloge koja ju je napravila, obično administratorske. Poziv iz aplikativnog koda izvršava telo van pozivaočevog RLS konteksta. Podrazumevano koristite SECURITY INVOKER; gde je definer funkcija zaista neophodna, postavite joj eksplicitan search_path i tretirajte je kao pregledanu bezbednosnu granicu, a ne kao pomoćni alat.

Neka garancija bude proverljiva

Svojstvo izolacije koje nijedan test ne može da obori jeste uverenje. Test koji je važan namerno je suprotstavljen: otvorite transakciju kao zakupac A, izvršite tačno one upite koje izvršava aplikacija, i tvrdite da vrsta zakupca B nema — pa ponovite sa namerno nepostavljenom sesijskom promenljivom i tvrdite da je rezultat prazan, a ne potpun.

BEGIN;
SET LOCAL app.current_tenant_id = '<zakupac-a>';
SELECT count(*) FROM documents;          -- očekivano: samo vrste A
ROLLBACK;

BEGIN;                                    -- bez SET LOCAL
SELECT count(*) FROM documents;          -- očekivano: 0, ne sve
ROLLBACK;

Pokrećite ga u CI-u nad bazom u koju su zasejana bar dva zakupca, i pokrećite ga kao aplikativna uloga, ne kao migraciona. Test koji prolazi kao superkorisnik ne dokazuje ništa.

Sloj 2 — vektorsko skladište

Qdrant, kao i većina vektorskih baza, nudi dva načina razdvajanja zakupaca: jednu deljenu kolekciju sa filtrom po polju tenant_id, ili kolekciju po zakupcu. Počeli smo od deljene kolekcije i pomerili se s nje. I razlozi i cena tog poteza pripadaju na videlo:

Zašto smo se pomerili. Filter po polju je ista arhitektura kao WHERE tenant_id = ? — granicu sprovodi autor upita. Pogrešan ili nedostajući filter vraća susede pogrešnog zakupca. Sa kolekcijom po zakupcu, upit uperen na pogrešno mesto dobija 404 od mašine umesto tuđih dokumenata. Manji HNSW grafovi uz to dosežu svoje susede u manje skokova, a kolekcija može da se prikuje za određeni čvor ili region kada jedan regulisan zakupac ima zahteve o rezidentnosti podataka koje ostali nemaju.

Šta to košta. Nije besplatno i tim koji preuzima obrazac mora da uračuna cenu. Svaka kolekcija nosi sopstvene indeksne strukture i sopstveni memorijski prag, pa je dugačak rep malih zakupaca osetno manje efikasan od jednog deljenog grafa — zamisao sa deljenom kolekcijom postoji iz dobrih razloga pri velikom broju zakupaca. Uspostavljanje, rezervne kopije, migracije i izmene šeme sada se vrte kroz kolekcije umesto da se izvrše jednom. Analitika nad celim korpusom postaje razgranati upit. Ako je vaš broj zakupaca u hiljadama, a regulatorna površina mala, deljena kolekcija sa filtrom po polju i strogo pregledanim upitnim slojem može biti bolji kompromis — ali onda svojim klijentima precizno recite da ono što imate jeste filtriranje.

Šta „fail-closed" ovde konkretno znači

  • API zaboravi SET LOCAL → RLS politika poredi sa NULL → nijedna vrsta.
  • Upit dosegne pogrešno ime kolekcije → mašina vraća 404, a ne vektore drugog zakupca.
  • Oba sloja otkažu istovremeno → pretraga ne vraća ništa, pa u promptu nema konteksta drugog zakupca koji bi model sintetizovao.

Svaki otkaz je vidljiv u dnevnicima i u kvalitetu odgovora, umesto da bude tih.

Od čega ovo ne štiti

Dve granice na nivou mašine zatvaraju jednu klasu otkaza. Same po sebi nisu priča o zaštiti podataka, i upravo predstavljanje da jesu jeste tačka na kojoj se ovakva arhitektura preprodaje:

  • Sve van upitne putanje. Rezervne kopije, obnavljanja, replike, izvozi, analitički cevovodi i administratorski alati često rade kao uloge koje zaobilaze RLS. Svako od njih traži sopstveni odgovor.
  • Dnevnici i tragovi. Beleženje promptova i pretrage je način na koji otklanjate greške u RAG sistemu i na koji ispunjavate obaveze rekonstrukcije incidenta — i ujedno je druga kopija sadržaja zakupca, obično u skladištu bez ijedne od ovih kontrola.
  • Keševi i deljeno izvedeno stanje. Keš ugnježdavanja (embeddings), keš ponovnog rangiranja ili semantički keš ključiran po sadržaju umesto po sadržaju-i-zakupcu rado će jednom zakupcu poslužiti rezultat izračunat za drugog.
  • Sklapanje u aplikaciji. Ako orkestracioni sloj ispravno pretražuje po zakupcu pa spoji kontekste — za funkciju poređenja, panel „srodni dokumenti", agenta koji upituje dvaput — izolacija se završava tamo gde se završava odgovornost mašine.
  • Obuka i dodatno podešavanje modela. Podaci razdvojeni u mirovanju a pomešani u skupu za obuku više nisu razdvojeni.
  • Ubrizgavanje unutar jednog zakupca. Ove kontrole ne govore ništa o zlonamernom dokumentu u korpusu jednog zakupca koji upravlja agentom tog istog zakupca.
  • Ljudi i platforma. Operateri sa pristupom bazi, i sam model hostovanja, nalaze se sasvim van ove granice. To je drugi razgovor — onaj o tome gde podaci zaista rade.

Kratka kontrolna lista

  1. Aplikacija se povezuje kao uloga koja ništa ne poseduje i nema BYPASSRLS.
  2. ENABLE i FORCE ROW LEVEL SECURITY na svakoj tabeli vezanoj za zakupca.
  3. SET LOCAL unutar eksplicitne transakcije; pooler u transakcionom režimu.
  4. current_setting(…, true) da zakupac koji nedostaje otkaže zatvoreno, a ne greškom.
  5. Podrazumevano SECURITY INVOKER; svaka definer funkcija pregledana i sa prikovanim search_path.
  6. Test u CI-u koji bez postavljenog zakupca tvrdi prazno, pokrenut kao aplikativna uloga.
  7. Vektorska izolacija izabrana promišljeno — sprovedena mašinom ili filterom — i klijentima opisana rečima koje su tačne.
  8. Zapisan spisak svake putanje koja čita podatke zakupca van ciklusa zahteva, svaka sa sopstvenom kontrolom.

Naknadno ugrađivanje bilo čega od ovoga u aplikaciju koja radi znači prehodati svaki upit, svaku migraciju i svaki posao. Prvog dana je dramatično jeftinije nego u osamnaestom mesecu — i to je pravi razlog da se odlučite rano, pre nego što postoji zakupac čije biste podatke premeštali.

Ovaj tekst je redigovao naš automatizovani bezbednosni cevovod kako bi uklonio unutrašnje detalje topologije.