← Alla inlägg

ARKITEKTUR

Åtkomstkontroll i RAG: vem ser vad

När företagets dokument lämnas över till ett RAG-system följer den befintliga behörighetsmodellen i de flesta installationer inte med. En praktisk anteckning om vilket lager som upprätthåller åtkomsten, hur identiteten når fram till frågan och när en ändrad behörighet faktiskt landar i indexet.

Dokumenten i en organisation är aldrig en platt hög. HR-mappen öppnar inte vem som helst, juridisk korrespondens är avgränsad till en namngiven lista, ekonomiavdelningens kalkylblad har en räknebar krets läsare. Den strukturen har vuxit fram under år på filservrar, i SharePoint-bibliotek, i LDAP-grupper och i SSO-roller. Sedan lämnas samma dokument över till ett RAG-system. Första frågan att ställa är vart den befintliga behörighetsmodellen tog vägen under överlämningen.

I de flesta första installationer är svaret: ingenstans. Dokumentet delas i chunkar, bäddas in och skrivs till ett enda index. Behörighetsdatan från källsystemet följer inte med genom något av de stegen, och därmed är själva indexet ett plan utan behörigheter. Det här är en praktisk anteckning om hur lagren läggs tillbaka.

Hur ett enda index plattar ut behörighetsmodellen

I den enklaste installationen körs likhetssökningen över hela korpusen. Tillbaka kommer de k närmaste chunkarna, och vid frågetillfället säger ingenting vilket bibliotek, vilken mapp eller vilken åtkomstlista de kom från. Chunkarna går till modellen som kontext, och modellen behandlar allt den får som möjligt att svara på. Innehållet i en fil som användaren aldrig hade kunnat öppna dyker upp igen i svaret, omskrivet på naturligt språk.

OWASP Gen AI Security Project tar upp det här under LLM08:2025, Vector and Embedding Weaknesses: delas en enda vektordatabas i multitenant-miljöer eller miljöer med flera sekretessnivåer förs kontext över mellan användare eller frågor (context leakage). I praktiken handlar det inte om modellens beteende utan om klassisk auktorisation, och posten diskuteras vanligen tillsammans med klassen Broken Access Control i OWASP Top 10. Den första rekommenderade åtgärden är lika direkt: finkorniga åtkomstkontroller och behörighetsmedvetna vektor- och embedding-databaser, med strikt logisk och behörighetsmässig uppdelning av datamängderna inne i databasen.

OWASP RAG Security Cheat Sheet går ett steg längre och kräver att metadatan ligger på chunk-nivå i stället för på dokumentnivå: klassificering, ägare, behöriga roller och behöriga tenants står bredvid varje enskild chunk i vektordatabasen. Skälet är praktiskt. En behörighet som bara hänger på dokumentet kan tappa sin koppling under chunkningen, och kvar blir chunkar som det inte går att filtrera på.

Var filtret tillämpas

Åtkomstkontrollen kan upprätthållas på tre skilda ställen, och de tre är inte likvärdiga.

  • Uppdelning vid indexeringen: olika sekretessnivåer skrivs till separata index, separata collections eller separata namespaces. Det ger den starkaste isoleringen, eftersom innehåll utanför behörigheten helt enkelt inte finns i den mängd som genomsöks. Priset är flexibilitet — en fråga som spänner över blandade behörigheter måste fördelas över flera index, och ändras behörigheterna i källsystemet krävs en omflyttning.
  • Metadatafilter vid frågetillfället: innehållet ligger kvar i ett index och filterpredikatet körs tillsammans med sökningen. Driftskostnaden är lägst här, eftersom innehållet stannar på ett ställe och filtret arbetar inne i motorn.
  • Filtrering efter retrieval: sökningen körs ofiltrerad och listan som kommer tillbaka kortas i applikationskoden. Säkerhetsmässigt är det den minst robusta positionen, och den sänker dessutom recall mätbart.

Azure AI Search namnger den här skillnaden formellt genom parametern vectorFilterMode (se Azure AI Search — Filters in vector search). I läget preFilter tillämpas filterpredikatet under traverseringen av HNSW-grafen, och dokumentationen slår fast att prefiltrering garanterat ger k resultat om de finns i indexet. I läget postFilter traverseras varje shard utan filter och predikatet tillämpas efteråt; vid starkt selektiva filter sänker det recall och ger falskt negativa resultat. strictPostFilter, som finns i förhandsversion, filtrerar först efter att den globala top-k-mängden är bestämd och kan ge noll resultat vid selektiva filter. Index som skapats efter ungefär 15 oktober 2023 använder preFilter som standard.

Slutsatsen som betyder något här: säkerhetsfilter är typiskt starkt selektiva. En enskild användare ser i regel en liten procentandel av korpusen. Det är precis i det läget som postfiltrering tyst returnerar färre träffar än som faktiskt finns. Prefiltrering betalar korrektheten i latens, och Microsoft har publicerat siffrorna i samma dokument: vid 1 miljon vektorer och 1536 dimensioner är prefiltrering ungefär 30 % långsammare när mer än 30 % av datamängden filtreras, och ungefär 7 gånger långsammare när mindre än 2 % filtreras. Vid 100 000 vektorer är prefiltrering ungefär 50 % långsammare när filtreringen ligger under 0,1 %. Latensbudget och säkerhetsmodell måste planeras tillsammans.

Säkerhetsfiltrering med metadata

Det vanligaste konkreta mönstret skriver de behöriga gruppidentifierarna till varje chunk som en filtrerbar collection. I Azure AI Searchs mönster för säkerhetsfilter (Azure AI Search — Security filters for trimming results) definieras fältet så här.

json
{
  "name": "group_ids",
  "type": "Collection(Edm.String)",
  "filterable": true,
  "retrievable": false
}

Att retrievable står på false är avsiktligt: behörighetsmetadatan finns för att filtrera med, den ska inte tillbaka i svarskroppen. Vid frågetillfället har filtret den här formen.

text
group_ids/any(g: search.in(g, 'grp-finance, grp-legal-readers'))

Samma dokumentation påpekar att en likhetskedja av formen Id eq ’id1’ or Id eq ’id2’ i stället är svår att hålla korrekt och arbetsam att förvalta, och att svarstiderna vid hundratals eller tusentals värden vandrar upp i storleksordningen många sekunder, medan search.in förväntas svara under sekunden. Samma källa anger gränsen lika tydligt: via säkerhetsprincipalen sker varken autentisering eller auktorisation, principalen är bara en sträng. Filtret förutsätter alltså att identiteten härletts korrekt längre upp; autentiseringen hör fortfarande hemma i lagret ovanför.

På den relationella sidan gör Postgres row level security samma jobb. Supabases pgvector-anvisning (Supabase — RAG with Permissions) lägger policyerna på tabellen document_sections, och den semantiska sökningen fortsätter att följa dem.

sql
alter table document_sections enable row level security;

create policy "read permitted sections"
on document_sections for select
to authenticated
using (
  document_id in (
    select id from documents
    where owner_id = auth.uid()
  )
);

Identiteten kommer in via auth.uid() över REST, eller via en sessionsvariabel från current_setting() vid en direkt Postgres-anslutning. Dokumentationen lägger till en konkret reservation: med en Foreign Data Wrapper är RLS känsligt för latens, och en analys av frågeplanen innan skarp drift är ett krav.

Valet mellan att filtrera i motorn och att korta listan i applikationen handlar inte bara om hastighet. Azures motivering lyder: filtrering i motorn tar dessutom bort egen kod för att lösa upp nästlade grupper och traversera ACL:er i flera nivåer. När den koden väl är skriven blir den en andra kopia av organisationens behörighetsmodell, och två kopior glider isär med tiden.

Att föra identiteten in i frågan

Var filtervärdet kommer ifrån väger lika tungt som filtret självt. Litar du på grupplistan som klienten skickar upprätthåller filtret ingenting: kortningen vilar då på ett värde som anroparen själv bestämmer. Det korrekta flödet härleder identiteten ur en verifierad token.

På Azure AI Searchs inbyggda ACL-väg (Azure AI Search — Document-level access control) följer användartoken med i headern x-ms-query-source-authorization. Tjänsten plockar ut claims för användare, grupp och scope ur token, jämför dem med behörighetsmetadatan i indexet och returnerar bara de dokument som användaren har behörighet till. Dessutom kontrolleras att den anropande applikationen har rollen Search Index Data Reader — kontrollen sker alltså i två steg. De här funktionerna ligger i en förhandsversion av REST-API:et; verifiera den exakta versionssträngen mot aktuell dokumentation på Microsoft Learn innan du skriver in den i ett anrop. Tjänsten dokumenterar fyra officiella tillvägagångssätt: säkerhetsfilter (GA, oberoende av API), POSIX-liknande ACL:er med RBAC-scope, känslighetsetiketter från Microsoft Purview och SharePoint-M365-ACL:er, där de tre sista finns i förhandsversion.

På Google Vertex AI Search (Google Cloud — Vertex AI Search: data source access control) anges ACL:erna vid ingestion genom fältet acl_info i dokumentmetadatan, med strukturen readers → principals → group_id eller user_id. Identiteten kommer från Google Identity eller Workforce Identity Federation (Microsoft Entra ID, Okta, Ping), och attributet google.subject måste mappas mot e-postfältet hos den externa leverantören. Två gränsvärden formar konstruktionen direkt: 3 000 läsare per dokument, och att ACL-konfigurationen bestäms när ett data store skapas och inte går att ändra efteråt.

Vad som händer när behörigheter ändras

Det här är den del av arkitekturen som oftast förbises. När en användare tas bort ur en grupp får det verkan direkt i källsystemet, men inte i indexet. Azures dokumentation skriver ut det uttryckligen: det uppstår en tidsfördröjning innan förhands-API:et känner igen ändringar i de här åtkomst- eller behörighetsbegränsningarna. Behörighetsändringar i källsystemet — gruppmedlemskap i Entra, ADLS Gen2-ACL:er, tilldelade Purview-etiketter, SharePoint-ACL:er — slår igenom i sökresultaten först när den metadatan har synkroniserats till indexet, via en indexerkörning, en uppdatering över push-API:et eller en förnyad inläsning från Purview.

För SharePoint är skillnaden ännu finare. Ändringar på objekt med egna behörigheter fångas upp inkrementellt vid varje lyckad indexerkörning, medan ändringar som ärvs från en överordnad nivå — webbplats, bibliotek, lista eller mapp — kräver en uttrycklig förnyelse: /resync med options: ["permissions"], eller /resetdocs. En behörighet som stramats åt på biblioteksnivå vandrar därför inte nödvändigtvis vidare av sig själv, även om indexeraren går enligt schema.

Elasticsearch löser samma uppgift med en annan arkitektur (Elasticsearch — Document level security and connector access control sync). Det finns två skilda synktyper: innehållssynk till ett index med prefixet search- och åtkomstkontrollsynk till ett dolt index med prefixet .search-acl-filter-<INDEX-NAME>. Fältet i innehållsdokumenten heter _allow_access_control; åtkomst ges så snart minst en post i användarens åtkomstkontrolldokument stämmer med en post i det fältet, och ett tomt värde håller dokumentet stängt för alla.

json
{
  "title": "Q3 procurement note",
  "body": "...",
  "_allow_access_control": [
    "group:finance-readers",
    "group:procurement"
  ]
}

Den dokumenterade metoden vid behörighetsändringar är att ge Elasticsearch-API-nyckeln en expiration och schemalägga återkommande åtkomstkontrollsynkar; ändras en användares behörigheter behöver API-nyckeln uppdateras eller skapas om. I det allmänna DLS-beteendet gäller att om en användare har flera roller mot samma index kombineras rollfrågorna med OR — en enda matchande roll gör dokumentet synligt. DLS gäller inte skriv-API:er, och eftersom det körs vid varje fråga kostar det något i prestanda.

Designregeln som följer är kort: uppdateringsintervallet är en säkerhetsparameter. Tiden det tar för en behörighetsändring att nå indexet hör hemma skriftligt i systemets specifikation, och den väljs per sekretessnivå.

Att föra behörighetsmetadatan genom chunkningen

Partitioneringsstrategin i multitenant-databaser hör till samma bild. Qdrant rekommenderar en enda collection per embedding-modell med partitionering över payloaden (Qdrant — Multitenancy); från v1.11.0 lägger parametern is_tenant: true på ett keyword-index en tenants vektorer tillsammans, och i stor skala ger payload_m i hnsw_config kombinerat med globalt m satt till 0 en indexering som är oberoende per tenant. Samma dokumentation noterar att globala frågor utan gruppfilter blir långsammare eftersom de måste gå igenom alla grupper. Pinecone rekommenderar namespaces för att isolera tenants och definierar metadataoperatorerna $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $and och $or, där bara $and och $or är tillåtna på filteruttryckets översta nivå. Weaviate isolerar varje tenant i en egen shard med ett eget vektorindex. Amazon Bedrock Knowledge Bases bygger åtkomstkontrollen av metadatafilter och tillåter upp till 10 KB egen metadata per dokument.

Vilken databas som än väljs finns ett konfigurationssteg som ofta förbises. Azures dokumentation noterar att när ett skillset delar dokumentet i chunkar — Text Split skill eller integrerad vektorisering — måste fälten med behörighetsmetadata föras vidare från indexerarens field mappings in i index projections. För Purview-etiketter är formuleringen uttrycklig: utan den projektionen filtreras inte referenserna på chunk-nivå. Är projektionen inte satt tappar chunkningssteget behörighetsmetadatan, och kvar blir ofiltrerade chunkar. Utifrån syns ingenting av det: indexet byggs, sökningen går, det är bara filtret som inte får något fäste.

Spårbarhet och genereringssteget

Den andra halvan av åtkomstkontrollen är noteringen av vilken fråga som rört vilket dokument. OWASP RAG Security Cheat Sheet räknar upp den punkt för punkt: varje retrieval loggas med identiteten på den agent eller användare som frågar och med åtkomstkontrollmetadatan för de chunkar som kom tillbaka, och loggningen täcker hela pipelinen — mottagen fråga, hämtade chunkar med dokument-ID:n och åtkomstkontrollmetadata, den sammansatta modellindatan, den genererade modellutdatan och alla verktygsanrop som utlösts. Punkten som oftast hoppas över är cachen: cacheträffar ska loggas lika utförligt som färska retrievals, annars bryts spåret. Varje infogning, uppdatering och borttagning i indexet hör hemma i loggen med tidsstämpel och identiteten som utförde den. Den fjärde åtgärden i LLM08:2025 pekar åt samma håll: utförliga och oföränderliga loggar över retrieval-aktiviteten.

Det sista lagret är genereringen. Även med korrekt filtrerad retrieval kan ett flöde i flera steg dra in innehåll utanför den godkända omfattningen i svaret, via en sammanfattning, en mellanliggande anteckning eller en verktygsutdata. OWASP:s normativa åtgärder för det här steget: validera alla modellutdata innan de returneras, tillämpa policyfilter och maskning för personuppgifter och hemligheter, maskera dynamiskt utifrån den frågande användarens behörighetsnivå, och signera källhänvisningsdatan så att den inte går att ändra i efterhand. I praktiken betyder det att varje chunk som ett svar vilar på för sin identifierare vidare, och att den avslutande kontrollen körs i ett lager som är oberoende av modellen som producerade svaret.

Om innehåll som slår igenom via en sammanfattning finns ingen känd offentlig mätstudie. Behandla det som ett designantagande snarare än ett empiriskt fynd: allt som hamnar i modellens kontext kan dyka upp i svaret, och därför hör kontrollen hemma i retrieval-lagret.

Åtkomstkontroll i RAG är ingen ny säkerhetsdisciplin. Det är den befintliga behörighetsmodellen som lyfts över på en ny dataväg. Det hela kokar ner till fyra frågor. Ligger behörighetsmetadatan på chunk-nivå? Tillämpas filtret inne i motorn, under söktraverseringen? Härleds identiteten ur en verifierad token i stället för att levereras av klienten? Och hur många minuter går det mellan en behörighetsändring i källsystemet och dess verkan i indexet? Ett system som kan besvara de fyra skriftligt går att granska. Där de står öppna står också frågan om vad indexet returnerar öppen.

Källor

  • OWASP Gen AI Security Project — LLM08:2025 Vector and Embedding Weaknesses
  • OWASP — RAG Security Cheat Sheet
  • Azure AI Search — Filters in vector search (vectorFilterMode, prefilter/postfilter latency figures)
  • Azure AI Search — Security filters for trimming results
  • Azure AI Search — Document-level access control (ACLs, RBAC scopes, Purview labels, SharePoint indexer)
  • Supabase — RAG with Permissions (pgvector and row level security)
  • Google Cloud — Vertex AI Search: data source access control
  • Elasticsearch — Document level security and connector access control sync
  • Qdrant — Multitenancy
  • Pinecone — Namespaces and metadata filtering
  • Weaviate — Multi-tenancy
  • Amazon Bedrock Knowledge Bases — Metadata filtering

Fler inlägg

Du har ett system att bygga — eller ett som ska ta nästa steg.

No deck needed. 20 minutes. The rest is up to you.