Практическое руководство
Контроль доступа в RAG: где применять фильтры, чтобы модель не увидела чужие документы
Удалить запрещённые фрагменты после поиска недостаточно. К этому моменту они уже могли попасть в результаты ретривера, трассировку, общий кэш или запрос к модели. Безопасный конвейер уменьшает область поиска заранее, повторно проверяет каждый кандидат и не сохраняет текст там, где границы доступа нельзя гарантировать.
Граница безопасности проходит перед поиском
RAG — схема, в которой приложение извлекает релевантные материалы и добавляет их в контекст генеративной модели. В такой системе защищать нужно не только итоговый ответ. Закрытый текст считается раскрытым уже тогда, когда его увидел компонент, не предназначенный для этого пользователя.
Рассмотрим распространённую ошибку: ретривер выполняет глобальный поиск, получает двадцать фрагментов, а приложение оставляет пять разрешённых. Пользователь может не увидеть остальные пятнадцать в ответе, однако они могли сохраниться в диагностическом событии, попасть в кэш результатов или быть отправлены reranker-сервису. Если фильтр установлен после этих операций, он не является границей доступа.
Авторизация должна ограничивать множество кандидатов до чтения текста документа. Проверка перед формированием контекста — дополнительный барьер, а не замена раннему фильтру.
Целевой конвейер
- Шлюз устанавливает личность пользователя по проверенному серверному сеансу.
- Сервис политик вычисляет область доступа: арендатора, группы, атрибуты и версию политики.
- Ретривер выполняет поиск только внутри этой области.
- Авторизатор повторно проверяет идентификаторы найденных документов по актуальным правилам.
- Только разрешённые фрагменты загружаются и передаются reranker-компоненту.
- Сборщик контекста проверяет лимиты, происхождение и решение авторизатора.
- Кэш, журналы, цитаты и загрузка оригиналов сохраняют ту же границу доступа.
В примерах ниже используются условные идентификаторы и нейтральные конфигурации. Они показывают контракт между компонентами, но не являются конфигурацией конкретной векторной базы или готового продукта.
Шаг 1. Получайте идентичность только из доверенной среды
Клиентский запрос может содержать текст вопроса, но не должен назначать себе пользователя,
арендатора или группы. Эти данные шлюз извлекает из проверенного токена либо серверного сеанса.
Переданные клиентом заголовки вида X-User допустимы только за доверенным прокси,
который удаляет входное значение и устанавливает собственное.
{
"principal": {
"subject_id": "user-example-17",
"tenant_id": "tenant-example",
"groups": ["group-engineering"],
"issuer": "https://identity.example"
},
"policy_version": "policy-example-31"
}
Все значения в этом фрагменте — примеры. Стабильными ключами служат идентификатор субъекта, арендатор и издатель идентичности. Электронная почта и отображаемое имя для этой роли не подходят: они могут измениться или совпасть.
При недоступности каталога групп или неполной идентичности используйте отказ по умолчанию. Нельзя заменять неизвестный набор групп пустым набором с последующим обходом проверки либо считать пользователя участником всех групп.
Шаг 2. Храните границу доступа рядом с каждым фрагментом
При индексации перенесите правила исходного документа в метаданные каждого фрагмента. Одной ссылки на документ недостаточно, если поисковый движок сначала читает глобальный индекс, а авторизатор вызывается позднее.
{
"chunk_id": "doc-example-42:version-8:chunk-3",
"document_id": "doc-example-42",
"document_version": "version-8",
"text": "Условный текст для локальной проверки.",
"access": {
"tenant_id": "tenant-example",
"visibility": "restricted",
"allow_subjects": ["user-example-17"],
"allow_groups": ["group-engineering"],
"deny_subjects": [],
"policy_version": "policy-example-31"
}
}
Минимально полезные поля — арендатор, видимость, разрешённые субъекты или группы, явные запреты и версия политики. Если правило источника нельзя точно выразить средствами индекса, запишите более узкий предварительный фильтр, а точное решение оставьте сервису политик. Упрощение не должно расширять аудиторию.
Обновляйте текст и права как одну версию. Сначала подготовьте все фрагменты новой версии, затем атомарно переключите активную версию. Иначе поиск может совместить новый текст со старой политикой доступа.
Шаг 3. Компилируйте фильтр на сервере
Фильтр строится из доверенного контекста, а не принимается в готовом виде от браузера. Обязательное ограничение арендатора применяется всегда. После него добавляются разрешения и явные запреты.
retrieval:
candidate_scope:
all:
- equals:
field: access.tenant_id
value_from: principal.tenant_id
- any:
- equals:
field: access.visibility
value: public
- contains:
field: access.allow_subjects
value_from: principal.subject_id
- overlaps:
field: access.allow_groups
values_from: principal.groups
- not_contains:
field: access.deny_subjects
value_from: principal.subject_id
on_missing_access_metadata: deny
on_unknown_operator: deny
on_policy_timeout: deny
max_candidates: 40
Операторы условны: перед внедрением нужно проверить семантику фильтрации выбранного индекса.
Важное свойство конфигурации состоит в том, что фильтр выполняется внутри поиска до получения
top_k, чтения текста и записи списка кандидатов.
Если движок поддерживает только постфильтрацию, его нельзя считать единственной границей безопасности. Разделите индекс по арендаторам или областям доступа, используйте отдельный защищённый слой идентификаторов либо выберите механизм, способный ограничить кандидатов до извлечения содержимого.
Шаг 4. Разделите поиск идентификаторов и чтение текста
Полезная архитектурная граница — сначала получать разрешённые идентификаторы и оценки, а текст загружать только после повторной авторизации. Так закрытый фрагмент не попадёт в reranker и контекст из-за устаревших метаданных индекса.
principal = identity_from_verified_session(request)
scope = policy_service.compile_scope(principal)
candidate_ids = index.search_ids(
query=request.query,
filter=scope.index_filter,
limit=40
)
decisions = policy_service.authorize_batch(
principal=principal,
resource_ids=candidate_ids,
action="rag.read"
)
allowed_ids = [
item.id for item in candidate_ids
if decisions[item.id].allowed
and decisions[item.id].policy_version == scope.policy_version
]
chunks = document_store.read_chunks(allowed_ids)
ranked = rerank(request.query, chunks)
context = build_context(ranked[:5])
Это псевдокод, а не привязка к библиотеке. Функция read_chunks должна принимать
только идентификаторы с положительным решением. При отсутствии решения, несовпадении арендатора,
неизвестной версии политики или тайм-ауте ресурс исключается.
Для сложных политик проверку удобно выполнять пакетом, чтобы не делать отдельный сетевой вызов
для каждого фрагмента. Решение следует связывать с субъектом, действием, ресурсом и версией
политики; булевого поля allowed без этого контекста недостаточно.
Шаг 5. Изолируйте кэш по контексту доступа
Ключ hash(question) опасен: ответ или найденные документы первого пользователя
могут быть возвращены второму. Безопасный ключ включает границу доступа и версии данных.
cache_key = hash(
tenant_id
+ subject_or_access_scope_id
+ policy_version
+ index_version
+ normalized_query
+ retrieval_config_version
)
Предпочтительнее кэшировать идентификаторы и оценки, а перед чтением текста снова выполнять авторизацию. Если кэшируется готовый контекст или ответ, запись должна быть изолирована как минимум по арендатору и области полномочий. При отзыве прав меняйте версию политики либо адресно инвалидируйте связанные записи.
Не включайте полный список групп в читаемый ключ и не записывайте его в журнал. Используйте непрозрачный идентификатор области доступа или серверный хеш. Хеш не отменяет необходимость контролировать доступ к самому хранилищу кэша.
Шаг 6. Сделайте журналы безопасными по умолчанию
Событие трассировки должно отвечать на вопрос «почему запрос был разрешён или отклонён», не копируя содержимое документа. Для диагностики обычно достаточно непрозрачных идентификаторов, количества кандидатов, версии политики, длительности этапа и кода решения.
{
"event": "rag_retrieval_completed",
"request_id": "request-example-9",
"tenant_id_hash": "opaque-example",
"policy_version": "policy-example-31",
"candidate_count": 12,
"authorized_count": 4,
"denied_count": 8,
"context_chunk_count": 4,
"decision": "completed"
}
Не записывайте текст запроса, фрагменты, промпт целиком, списки групп и ответы модели без отдельного обоснования, политики хранения и контроля доступа. Отладочный режим не должен автоматически ослаблять эти правила в рабочей среде.
Метрики агрегируйте по безопасным измерениям. Идентификатор документа с очень низкой частотой появления сам может раскрывать существование закрытого проекта, поэтому его не стоит превращать в общедоступную метку метрики.
Шаг 7. Защитите сборку контекста последним инвариантом
Сборщик контекста не должен принимать произвольные фрагменты. Каждый элемент сопровождается подтверждением решения, субъектом, версией политики и временем действия. Перед сериализацией в запрос к модели проверьте весь набор.
context_builder:
require_authorization_decision: true
require_same_tenant: true
require_current_policy_version: true
reject_mixed_principals: true
reject_missing_provenance: true
include_denied_metadata: false
include_debug_payloads: false
on_validation_error: deny_request
После сборки контекста не добавляйте «полезные» результаты из глобального кэша, истории другого сеанса или фонового поиска. История диалога тоже является данными: при смене пользователя, арендатора или области прав создавайте новый сеанс либо повторно авторизуйте все сохранённые ссылки.
Воспроизводимая проверка границ
Подготовьте синтетический набор без рабочих данных: два арендатора, по одному пользователю и
по одному уникальному маркеру в закрытом документе. Например,
ALPHA_PRIVATE_MARKER и BETA_PRIVATE_MARKER. Маркеры являются
тестовыми значениями, а не секретами.
- Пользователь Alpha должен находить публичный материал и свой закрытый маркер.
- Тот же пользователь не должен получать идентификатор, текст или цитату Beta.
- Запрос с подменённым
tenant_idв теле не должен менять серверный контекст. - После отзыва группы повторный запрос и обращение к кэшу не должны возвращать старый результат.
- При недоступности сервиса политик запрос должен завершаться отказом, а не поиском без фильтра.
- В трассировках не должно быть обоих маркеров и текстов закрытых фрагментов.
Следующие команды — безопасный шаблон для локального тестового стенда. Адрес и формат маршрутов нужно адаптировать к своему приложению; команды выполняют только чтение и не изменяют данные.
curl --fail --silent --show-error \
-H 'Authorization: Bearer LOCAL_ALPHA_TEST_TOKEN' \
--get 'http://127.0.0.1:8080/search' \
--data-urlencode 'q=ALPHA_PRIVATE_MARKER'
curl --fail --silent --show-error \
-H 'Authorization: Bearer LOCAL_ALPHA_TEST_TOKEN' \
--get 'http://127.0.0.1:8080/search' \
--data-urlencode 'q=BETA_PRIVATE_MARKER'
curl --fail --silent --show-error \
-H 'Authorization: Bearer LOCAL_ALPHA_TEST_TOKEN' \
-H 'X-Tenant-ID: beta-example' \
--get 'http://127.0.0.1:8080/search' \
--data-urlencode 'q=BETA_PRIVATE_MARKER'
Не используйте рабочие токены в истории команд. Для автоматической проверки передавайте короткоживущий локальный токен через защищённый механизм тестового окружения и не печатайте его.
Как проверить результат
Успешный тест означает не только отсутствие чужого текста в финальном ответе. Проверьте наблюдаемость каждого этапа:
- глобальный поиск без серверного фильтра не выполнялся;
- чужой
document_idне передавался в загрузчик текста и reranker; - сборщик контекста получил только элементы с актуальным разрешением;
- кэш Alpha нельзя прочитать из сеанса Beta;
- после отзыва доступа старая кэшированная запись перестала использоваться;
- маркеры отсутствуют в журналах приложения, трассировках и событиях ошибок;
- ошибка авторизатора приводит к контролируемому отказу без вызова модели.
Отдельно запустите проверку на малом top_k. Если разрешённые результаты исчезают
из-за того, что запрещённые документы заняли список кандидатов до постфильтрации, ранний фильтр
фактически не работает.
Типовые ошибки
Фильтрация только перед показом ответа
Модель, reranker и журналы уже могли получить закрытый текст. Исправление: ограничивать поиск до кандидатов и повторно авторизовать идентификаторы до чтения содержимого.
Доверие к арендатору из запроса
Пользователь меняет параметр и расширяет область поиска. Исправление: получать арендатора из проверенного сеанса и игнорировать клиентское значение при построении политики.
Общий семантический кэш
Похожие вопросы разных пользователей получают одну запись. Исправление: включать область доступа и версии политики в ключ, а перед чтением текста повторять авторизацию.
Устаревшие права в индексе
Пользователя удалили из группы, но старые метаданные продолжают разрешать поиск. Исправление: версионировать политики, быстро инвалидировать индекс и использовать актуальную повторную проверку перед загрузкой текста.
Полный промпт в трассировке
Средство наблюдаемости превращается в копию закрытого хранилища. Исправление: журналировать идентификаторы, количества и коды решений, а полезную нагрузку отключать по умолчанию.
Разрешение при ошибке зависимости
Тайм-аут авторизатора трактуется как отсутствие ограничений. Исправление: явно настроить отказ по умолчанию и проверить этот сценарий отдельным тестом.
Ограничения
Ранний фильтр не решает все задачи защиты данных. Он не исправляет ошибочно широкие права в исходной системе, не предотвращает утечку через уже скомпрометированный сервис и не заменяет шифрование, сетевую изоляцию, аудит или управление сроками хранения.
Атрибутные и контекстные правила могут зависеть от времени, устройства, местоположения или состояния документа. Их не всегда можно полностью закодировать в метаданных векторного индекса. В таком случае индекс должен применять консервативное ограничение, а актуальный авторизатор — окончательное решение.
Изоляция по отдельному индексу на арендатора упрощает доказательство границы, но увеличивает эксплуатационные расходы. Общий индекс экономичнее, однако требует строгого обязательного фильтра, тестов на обход и контроля всех путей чтения. Выбор зависит от модели угроз, числа арендаторов и допустимой сложности эксплуатации.
Контрольный список перед выпуском
- Личность и арендатор поступают из проверенного серверного контекста.
- Каждый фрагмент имеет версионированные метаданные доступа.
- Фильтр применяется внутри поиска до формирования
top_k. - Текст загружается только после повторной авторизации идентификатора.
- Reranker и модель не получают отклонённые фрагменты.
- Кэш связан с областью доступа и версией политики.
- Журналы не содержат запросы, фрагменты и полные промпты по умолчанию.
- Отсутствие метаданных, тайм-аут и неизвестное правило приводят к отказу.
- Отзыв доступа проверен вместе с кэшем и историей диалога.
- Синтетические маркеры отсутствуют во всех неразрешённых путях данных.
Дополнительные практические материалы собраны в разделе «Руководства», а определения архитектурных и эксплуатационных терминов — в глоссарии.