Izolacija najemnikov v RAG sistemih: mejo potisnite v same pogone za shranjevanje
Praktični vodnik za uveljavljanje meje med najemniki v RAG platformi pod aplikacijsko plastjo — PostgreSQL Row-Level Security in štirje obhodi, ki jo tiho razveljavijo, vektorske zbirke na najemnika in kaj v resnici stanejo, test, ki lahko pade, in pošten seznam tega, pred čimer nič od tega ne varuje.
- arhitektura
- varnost
- rag
Zakaj je WHERE tenant_id = ? filtriranje in ne izolacija
Prvo vprašanje, ki ga vsak CISO postavi o večnajemniški platformi za pridobivanje (retrieval), je, kako preprečite, da bi dokumenti enega najemnika pristali v odgovorih drugega. Običajni odgovor kaže na aplikacijsko plast: vmesna programska oprema razčleni žeton, izlušči identifikator najemnika, ORM pa vsakemu poizvedovanju doda pogoj.
To je filtriranje. Deluje natanko tako dolgo, dokler vsako poizvedovanje, v vsaki kodni poti, ki jo napiše vsak inženir, ki se bo kdaj dotaknil te kode, ne pozabi nositi tega pogoja. Načini odpovedi so vsakdanji, ne eksotični:
- Nadgradnja knjižnice spremeni napačno oblikovan žeton iz trde napake v opozorilo in zahteva se nadaljuje z ničelnim identifikatorjem najemnika.
- Neposredna poizvedba SQL, napisana za poročevalsko končno točko, obide plast ORM, ki je nosila filter.
- Zapleten
JOINpovzroči, da ORM izpusti pogoj v podpoizvedbi. - Opravilo v ozadju — ponovno indeksiranje, migracija, izvoz — se sploh izvede brez konteksta zahteve.
Nič od tega ni slab inženir. To je statistični izid varnostnega modela, ki mora biti pravilno znova implementiran v vsaki prihodnji vrstici kode. V sistemu RAG je posledica hujša od običajnega uhajanja podatkov: ušle vrstice se ne pojavijo le v odgovoru, temveč se sestavijo v poziv (prompt), model pa podatke konkurenta povzame naravnost v bralčev delovni tok. Ni očitno napačnega izpisa, ki bi to ujel.
Alternativa je, da mejo premaknete pod aplikacijo, v pogone, ki hranijo podatke — tako da napaka v aplikaciji povzroči viden zlom namesto tihega uhajanja. V EinCoreRAG smo to storili na dveh plasteh neodvisno, relacijski in vektorski, tako da odpoved na eni ne more doseči podatkov druge. Spremljajoča epizoda podkasta (v angleščini) pokriva razmislek; ta zapis je izvedbena podrobnost, vključno z deli, ki grizejo.
Plast 1 — PostgreSQL Row-Level Security
Mehanizem je majhen. Pasti okoli njega niso.
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);
Vsaka transakcija se začne z nastavitvijo najemnika samo za to transakcijo:
SET LOCAL app.current_tenant_id = '…';
Štiri podrobnosti v teh petih vrsticah nosijo celotno zasnovo.
SET LOCAL, ne SET. Vsaka produkcijska namestitev postavi pred Postgres združevalnik povezav (pooler), ta pa multipleksira mnogo odjemalskih zahtev čez nekaj dolgoživih povezav. Navaden SET piše v sejo, ki preživi zahtevo in pripada tistemu, ki dobi to povezavo naslednji. SET LOCAL vrednost omeji na transakcijo in Postgres jo ob potrditvi ali povrnitvi počisti, kar pusti povezavo čisto za naslednjega uporabnika. Če uporabljate PgBouncer, to pomeni tudi, da potrebujete združevanje na ravni transakcij in vse delo znotraj izrecne transakcije — v načinu združevanja po stavkih lahko SET LOCAL in stavki, ki so od njega odvisni, pristanejo na različnih zaledjih.
true v current_setting. To je zastavica missing_ok. Brez nje zahteva, ki je pozabila nastaviti spremenljivko, sproži napako; z njo iskanje vrne NULL. Primerjava tenant_id z NULL ni nikoli resnična, zato pravilnik ne ujame nobene vrstice. Poizvedba uspe in ne vrne ničesar. To je lastnost »fail-closed« in je namerna: glasen, prazen rezultat je varen izid, prazen rezultat tam, kjer so se pričakovali podatki, pa je nekaj, kar vidita tako nadzor kot testi.
FORCE ROW LEVEL SECURITY. Sam ENABLE pravilnikov ne uporabi za lastnika tabele. Aplikacije se zelo pogosto povežejo kot vloga, ki je lastnica sheme, ker je bilo tako nastavljeno migracijsko orodje — in ta vloga bere vsako vrstico, kot da RLS ne bi bilo. To je najpogostejši način, da ekipa uvede RLS, ga testira kot superuporabnik, vidi, da »deluje«, in v produkciji nima nobene izolacije. Bodisi se povežite kot vloga, ki ni lastnica in ima le pravice DML, bodisi dodajte FORCE — ali oboje. Vloge z atributom BYPASSRLS so prav tako izvzete, superuporabniki pa vedno — zato se nobena aplikacija ne sme povezati kot ena od njih.
Funkcije SECURITY DEFINER so luknja, ki jo izkopljete sami. Pomožna funkcija — obračunski izračun, ovojnica za iskanje — ustvarjena s SECURITY DEFINER, se izvaja s pravicami vloge, ki jo je ustvarila, običajno skrbniške. Klic iz aplikacijske kode izvede telo zunaj klicateljevega konteksta RLS. Privzeto uporabljajte SECURITY INVOKER; kjer je funkcija z definerjem res potrebna, ji nastavite izrecno search_path in jo obravnavajte kot pregledano varnostno mejo, ne kot pripomoček.
Naredite jamstvo preverljivo
Lastnost izolacije, ki je noben test ne more ovreči, je prepričanje. Test, ki šteje, je namerno nasproten: odprite transakcijo kot najemnik A, izvedite natanko tiste poizvedbe, ki jih izvaja aplikacija, in preverite, da vrstic najemnika B ni — nato ponovite z namerno nenastavljeno sejno spremenljivko in preverite, da je rezultat prazen in ne popoln.
BEGIN;
SET LOCAL app.current_tenant_id = '<najemnik-a>';
SELECT count(*) FROM documents; -- pričakovano: samo vrstice A
ROLLBACK;
BEGIN; -- brez SET LOCAL
SELECT count(*) FROM documents; -- pričakovano: 0, ne vse
ROLLBACK;
Poganjajte ga v CI proti bazi s posejanima vsaj dvema najemnikoma in ga poganjajte kot aplikacijska vloga, ne kot migracijska. Test, ki uspe kot superuporabnik, ne dokazuje ničesar.
Plast 2 — vektorska shramba
Qdrant, tako kot večina vektorskih baz, ponuja dva načina ločevanja najemnikov: eno skupno zbirko s filtrom po polju tenant_id ali zbirko na najemnika. Začeli smo s skupno zbirko in se od nje odmaknili. Razlogi in cena te poteze sodijo na plano oboji:
Zakaj smo se premaknili. Filter po polju je ista arhitektura kot WHERE tenant_id = ? — mejo uveljavlja avtor poizvedbe. Napačen ali manjkajoč filter vrne sosede napačnega najemnika. Z zbirko na najemnika poizvedba, ki cilja napačno mesto, dobi 404 iz pogona namesto tujih dokumentov. Manjši grafi HNSW poleg tega dosežejo svoje sosede v manj skokih, zbirko pa je mogoče pripeti na določeno vozlišče ali regijo, kadar ima en reguliran najemnik zahteve glede rezidenčnosti, ki jih drugi nimajo.
Kaj to stane. To ni zastonj in ekipa, ki vzorec prevzema, mora ceno vračunati. Vsaka zbirka nosi svoje indeksne strukture in svoj spominski prag, zato je dolg rep majhnih najemnikov občutno manj učinkovit od enega skupnega grafa — zasnova s skupno zbirko obstaja iz dobrih razlogov pri velikem številu najemnikov. Vzpostavljanje, varnostne kopije, migracije in spremembe sheme zdaj tečejo v zanki čez zbirke, namesto da bi se izvedle enkrat. Analitika čez celoten korpus postane razvejano poizvedovanje. Če je vaše število najemnikov v tisočih in je vaša regulatorna površina majhna, je skupna zbirka s filtrom po polju in strogo pregledano poizvedovalno plastjo lahko boljši kompromis — a takrat svojim strankam natančno povejte, da imate filtriranje.
Kaj »fail-closed« tukaj konkretno pomeni
- API pozabi
SET LOCAL→ pravilnik RLS primerja z NULL → nič vrstic. - Poizvedba doseže napačno ime zbirke → pogon vrne 404, ne vektorjev drugega najemnika.
- Obe plasti odpovesta hkrati → pridobivanje ne vrne ničesar, torej v pozivu ni konteksta drugega najemnika, ki bi ga model lahko povzel.
Vsaka odpoved je vidna v dnevnikih in v kakovosti odgovora, namesto da bi bila tiha.
Pred čim to ne varuje
Dve meji na ravni pogona zapreta en razred odpovedi. Sami po sebi nista zgodba o varstvu podatkov in prav predstavljanje, da sta, je točka, kjer se takšna arhitektura preproda:
- Vse zunaj poizvedovalne poti. Varnostne kopije, obnovitve, replike, izvozi, analitični cevovodi in skrbniška orodja pogosto tečejo kot vloge, ki obidejo RLS. Vsako od njih potrebuje svoj odgovor.
- Dnevniki in sledi. Beleženje pozivov in pridobivanja je način, kako razhroščite sistem RAG in kako izpolnite dolžnosti rekonstrukcije incidenta — in je druga kopija najemnikove vsebine, običajno v shrambi brez teh kontrol.
- Predpomnilniki in deljeno izpeljano stanje. Predpomnilnik vložitev (embeddings), predpomnilnik ponovnega razvrščanja ali semantični predpomnilnik s ključem po vsebini namesto po vsebini-in-najemniku bo enemu najemniku z veseljem postregel rezultat, izračunan za drugega.
- Sestavljanje v aplikaciji. Če orkestracijska plast pravilno pridobi po najemnikih in nato združi kontekste — za primerjalno funkcijo, ploščo »sorodni dokumenti«, agenta, ki poizveduje dvakrat — se izolacija konča tam, kjer se konča odgovornost pogona.
- Učenje in dodatno prilagajanje modelov. Podatki, ločeni v mirovanju in pomešani v učni množici, niso več ločeni.
- Vrivanje znotraj enega najemnika. Te kontrole ne povedo ničesar o zlonamernem dokumentu v korpusu enega najemnika, ki krmili agenta tega istega najemnika.
- Ljudje in platforma. Operaterji z dostopom do baze in sam model gostovanja so povsem zunaj te meje. To je drug pogovor — tisti o tem, kje podatki dejansko tečejo.
Kratek kontrolni seznam
- Aplikacija se poveže kot vloga, ki ni ničesar lastnica in nima
BYPASSRLS. ENABLEinFORCE ROW LEVEL SECURITYna vsaki tabeli, vezani na najemnika.SET LOCALznotraj izrecne transakcije; pooler v transakcijskem načinu.current_setting(…, true), da manjkajoč najemnik odpove zaprto in ne z napako.- Privzeto
SECURITY INVOKER; vsaka funkcija z definerjem pregledana in s pripetimsearch_path. - Test v CI, ki brez nastavljenega najemnika trdi prazno, pognan kot aplikacijska vloga.
- Vektorska izolacija izbrana premišljeno — uveljavljena s pogonom ali s filtrom — in strankam opisana z besedami, ki so resnične.
- Zapisan seznam vsake poti, ki bere najemnikove podatke zunaj cikla zahteve, vsaka s svojo kontrolo.
Naknadno vgrajevanje česarkoli od tega v delujočo aplikacijo pomeni prehoditi vsako poizvedbo, vsako migracijo in vsako opravilo. Prvi dan je dramatično ceneje kot v osemnajstem mesecu — in to je pravi razlog, da se odločite zgodaj, preden obstaja najemnik, čigar podatke bi premikali.
To objavo je redigiral naš avtomatizirani varnostni cevovod, ki je odstranil notranje topološke podrobnosti.