私有数据的检索权限:多租户下的向量库隔离
PinkStage
6 min read
多租户 RAG 系统里,检索权限和向量召回同等重要。召回做对了、隔离漏了,等于把 A 客户的数据喂给了 B 客户的查询。这类越权往往静默发生,比显式报错更危险——没人报错,但信息已经漏了。
隔离放在哪一层
三种典型层级,成本和安全性递增:
- 物理隔离:每个租户独立索引或独立库。最干净,但资源与运维成本高,适合强合规场景。
- 逻辑隔离:同一索引,每条向量带
tenant_id 等元数据,检索时强制过滤。成本低,前提是过滤真的不可绕过。
- 混合:热数据逻辑隔离,极敏感租户物理隔离。
多数业务选逻辑隔离,但逻辑隔离的可靠性完全取决于"过滤是否被强制执行"。一句"业务层记得传租户"是靠不住的,必须落在框架里。
强制过滤,不能靠调用方自觉
过滤必须落在检索封装内部,而不是让上层业务记得传 tenant_id。一旦某处漏传,越权就发生了。
def retrieve(query_vec, tenant_id, user_acl, top_k=10):
"""租户与文档级权限双重过滤,任何调用方都无法跳过。"""
filter_expr = {
"tenant_id": tenant_id,
"doc_id": {"in": user_acl.allowed_docs}, # 用户可见文档集合
}
hits = vector_store.search(
query_vec,
top_k=top_k * 3, # 多召回去过滤,避免过滤后不足
filter=filter_expr, # 向量与元数据联合过滤
)
return [h for h in hits if h.doc_id in user_acl.allowed_docs][:top_k]
关键点:top_k 要放大再截断。若先召回到精确 top_k 再过滤,可能被过滤掉大半,返回结果严重不足,逼着上层放宽条件——这正是越权的高发诱因。宁可多召回,再严格裁剪。
租户内的细粒度权限
租户不等于"全员可见"。一个组织内,不同角色能看的文档不同。权限要在检索前算成"可访问文档集合",作为硬过滤条件,而不是检索后裁剪。检索后再裁,越权文档已经进过上下文,风险已经产生,裁剪只是不让它出现在答案里,过程已经违规。
混合检索别漏掉一边
很多系统同时用向量召回和关键词/全文召回。两路都必须带同一套权限过滤。只在向量路加过滤、关键词路裸奔,是常见漏洞——攻击者只要构造一个恰好命中关键词的查询,就能绕过向量路的租户限制。统一在检索网关层注入过滤表达式,能避免各路各自实现导致的遗漏。
边界条件与踩坑
- 向量相似度不等于权限。语义相近的文档可能分属不同租户或不同角色,绝不能因为"相关"就放松过滤,相关性判断和权限判断是两套独立逻辑。
- 缓存要带租户键。把检索结果按 query 缓存却不分租户,下一租户可能命中上一租户的结果,缓存层是越权高发区。
- 权限变更要同步元数据。用户被回收某文档权限后,若向量元数据没更新,旧过滤仍会放行,形成"权限已收、检索未收"的时间窗。
- 过滤表达式别用字符串拼接,防止注入把
tenant_id 条件破坏掉;用结构化参数,让框架负责转义。
- 审计不能少。谁在什么时间检索到了什么文档,留痕便于事后追溯越权,也是合规检查的基本项。
- 加密与密钥隔离要配合:逻辑隔离解决"检索不到",密钥隔离解决"即使拿到存储也读不出",两者互补,尤其在备份和跨区复制场景。
性能权衡
过滤下推(在索引层联合过滤)通常比"先召回大量再内存过滤"更省,但部分向量库对带过滤的召回有配额限制,需要实测召回率。经验上,放大召回倍数 + 严格后过滤,是在准确率和安全性之间最稳的折中。若过滤维度很多(租户 + 角色 + 文档标签),要考虑过滤条件组合爆炸对索引性能的影响,必要时引入预计算的可访问集合。
密钥与加密隔离
逻辑隔离解决"检索不到",但存储层若共用且未加密,拿到磁盘或备份的人仍能读出全部租户数据。配合租户级密钥(每租户一把密钥,密钥由独立密钥管理服务托管),即使存储泄露,没有对应密钥也解不开。密钥的访问控制要和检索权限同源,避免"检索拦住了,密钥却共享了"的错位。备份、跨区复制这些旁路环节,往往是最先漏掉隔离的地方,要单独核查,不能只盯主链路。
隔离失效的典型时间窗
权限回收是最容易出事的点:管理员撤销某用户文档权限,应用层立刻生效,但向量元数据更新有延迟,或缓存未失效,这段窗口内旧权限仍可能放行。工程上要接受"权限变更不是瞬时全局生效",要么走最终一致性并标注窗口,要么在检索网关加一层实时权限查询兜底,宁可降低一点性能,也不能放过越权。另一个窗口是"新建文档未建索引期"——文档已可读但向量还没入库,检索会漏,要在业务层区分"已索引"和"待索引"状态,别让待索引文档被当成"不存在"。
越权检测的兜底
再严谨的过滤也可能有漏。一个实用的兜底是在答案生成后,做一次"引用来源归属校验":检查回答引用的每个文档是否真的在当前用户的可访问集合内。这一步独立于检索过滤,相当于末端的一道闸。它抓不到的是"模型凭记忆编造了越权内容",但能抓"检索层漏放、生成层直接用了"的大头。多层防御,比单层更扛事,尤其当系统规模扩大、过滤逻辑变复杂时,兜底闸的价值会越来越高。
补充:隔离策略要写进架构决策记录。它属于"平时看不见、出事要命"的部分,人员变动后最容易被人无意中改松。把"为什么这样隔离、动了会怎样"写清楚,比靠个人记忆可靠,也方便新成员接手时不踩坑。安全相关的设计如果不文档化,就会在每次重构里悄悄退化。
私有数据检索的底线是:权限过滤和相似度计算同样是一等公民,且必须不可绕过、可审计。漏掉一次,代价远不止一条错误回答。
1 view
0 likes
0 comments