The fastest way to turn a helpful RAG assistant into a security incident is to index everything and let similarity search decide who sees what. Embeddings do not know about permissions: if the CEO's compensation memo is in the index, a well-phrased query from anyone will retrieve it.
Tier: Advanced · Workflow area: RAG data pipelines
Access-control propagation is a data problem, and the data engineer owns it — source ACLs must travel with content into the index as filterable metadata, retrieval must enforce the caller's entitlements in the engine, and revocations must propagate on a measured SLA. This playbook builds and adversarially tests that path.
Extract ACLs at ingestion time, normalized to groups. Alongside content (pb-27), pull each document's permission principals from the source API and resolve users to durable group IDs where possible — group-based tags survive personnel churn; user-ID tags rot. Land them in the pb-31 schema's acl_tags:
-- pb-31 schema already has: acl_tags TEXT[] NOT NULL DEFAULT '{}'
UPDATE rag_chunks SET acl_tags = ARRAY['grp:finance', 'grp:exec']
WHERE doc_id = 'finance/comp-memo-2026.pdf';
Default-deny: a document whose ACL extraction fails gets acl_tags = ['quarantine'] and retrieves for no one — never an empty-means-public convention.
Enforce in the engine, not the application. Every retrieval carries the caller's entitlements and the filter is applied inside the store, on both hybrid legs (pb-33). With pgvector, row-level security makes the database itself the enforcement point:
ALTER TABLE rag_chunks ENABLE ROW LEVEL SECURITY;
CREATE POLICY chunk_acl ON rag_chunks USING (
tenant_id = current_setting('app.tenant_id')
AND acl_tags && string_to_array(current_setting('app.caller_groups'), ',')
);
def search(q, caller, k=10):
with pool.connection() as con:
con.execute("SET app.tenant_id = %s", (caller.tenant_id,))
con.execute("SET app.caller_groups = %s", (",".join(caller.groups),))
return hybrid_search(con, q, k) # RLS applies to both legs automatically
Dedicated-engine equivalents: Qdrant payload filters composed server-side into every query, Pinecone metadata filters per namespace, OpenSearch document-level security. The invariant is the same everywhere: no code path can reach vectors without the filter attached.
Propagate ACL changes through the sync pipeline. Permission changes arrive without content changes, so pb-36's content-hash diffing will skip them — add an ACL hash to change detection: classify() returns acl_changed when permission sets differ, triggering a metadata-only update (no re-embed, no cost). Revocations are deletes' sibling: dead-letter loudly on failure and track propagation lag as its own SLA metric.
Build the leakage test suite — the metric layer. Assertions, run as CI for the retrieval service and on a schedule in production:
Cover the side channels. Retrieval is not the only exit: cached answers generated for a privileged user must not serve to others (scope any answer/semantic cache by entitlement hash); logs and traces (pb-37) that store retrieved chunks inherit document sensitivity — restrict the observability store accordingly; and "no results" versus "you lack access" responses should be indistinguishable to avoid confirming a document's existence.
Filtered ANN search can silently degrade — some engines traverse the graph first and filter after, so a highly restrictive ACL filter may return fewer than k results even when k entitled matches exist. Re-run the pb-32 recall measurement with production ACL filters applied; secure-but-low-recall fails the positive control just as surely as leaks fail the negative one.
For client work, ACL propagation is usually the difference between a demo and a deployable system — so scope it explicitly in the SOW, and insist on integrating with the client's real identity provider (their groups, their revocation events) rather than a parallel permission table that will drift. Run the leakage suite on a synthetic corpus with synthetic users first and demo it to the client's security team; then re-run inside their environment against real ACLs before go-live, delivering the leak-rate result, the revocation SLA number, and the quarantine-default behavior as written evidence. Never accept "index only public documents" as a permanent posture without a tested enforcement mechanism — corpora drift, and today's public folder acquires tomorrow's confidential file.