Все статьи
· 10 мин чтения

Guardrails LLM Filter: чтобы твои секреты не уехали к LLM-провайдеру 🛡️🔑

Разбираю cloud-ru-tech/guardrails-llm-filter — открытый прокси, который маскирует PII и секреты по пути к LLM и возвращает оригиналы в ответе. Запустил демо, погонял карты, СНИЛС и ключи от AWS, прошёлся по всей веб-консоли: правила, песочница, теневой режим, аудит, мониторинг.

Guardrails LLM Filter: чтобы твои секреты не уехали к LLM-провайдеру 🛡️🔑

Guardrails LLM Filter: чтобы твои секреты не уехали к LLM-провайдеру 🛡️🔑

Ну чё, малютки, признавайтесь: кто хоть раз кидал в чатик с LLM кусок лога с боевым токеном? Или письмо клиента с его телефоном и картой — «перепиши повежливее»? Всё это уезжает провайдеру, оседает в логах на чужой стороне, а у безопасников дёргается глаз. А если у тебя ещё и агенты сами собирают контекст из базы и тикетов — ты вообще не контролируешь, что улетает в промпт.

Сегодня щупаем guardrails-llm-filter от Cloud.ru — открытый прокси, который решает ровно эту боль. Я его собрал, запустил демо, скормил ему фейковые карты и СНИЛСы и наделал скриншотов — всё покажу.

🔥Суть за 10 секунд

guardrails-llm-filter — прозрачный reverse-proxy между твоим приложением и LLM-провайдером. По пути к модели ~265 regex-правил заменяют PII и секреты на плейсхолдеры вида <EMAIL_1>, а в ответе возвращают оригиналы. Модель никогда не видит чувствительные данные, клиент никогда не видит заглушки. Один Go-бинарь, в приложении меняется только base URL. Лицензия Apache 2.0.

Apache 2.0Лицензия

Открытый код, один Go-бинарь без внешних зависимостей. Каталог правил секретов — производный от gitleaks.

~265 правилДетекция

Ключи API, токены, учётки БД, карты, IBAN — и российские PII: СНИЛС, ИНН, ОГРН с проверкой контрольных сумм.

F1 93.1pii-bench

Precision 99.9% на hivetrace/pii-bench — выше, чем у ML-пайплайна GLiNER Guard (84.4). И это чистые regex + валидаторы.


Как это работает

Идея наглая в своей простоте. Клиент ходит не к провайдеру, а к прокси. Тот сканирует запрос, меняет находки на плейсхолдеры, шлёт наверх. Ответ провайдера сканируется в обратную сторону — плейсхолдеры меняются на оригиналы. Таблица «заглушка → оригинал» живёт в памяти процесса ровно столько, сколько живёт запрос:

Весь путь — в одном обработчике: замаскировал, переслал, восстановил.

Ключевые свойства, за которые лично я ставлю плюсики:

1Drop-in. В приложении меняется только base URL. OpenAI (/v1/chat/completions, /v1/responses) и Anthropic (/v1/messages) — из коробки, JSON и стриминг.
2Двусторонность. Большинство инструментов «замазали и забыли» (у Portkey редакция вообще irreversible by design). Тут оригиналы возвращаются в ответ, в том числе в аргументах tool-call.
3Без ML на data-path. Regex + валидаторы контрольных сумм — микросекунды на запрос, никаких внешних вызовов и GPU.
4Fail-open. Любая внутренняя ошибка пропускает трафик, а не кладёт его. Запросы никогда не блокируются — сервис только маскирует.
5Российские PII. СНИЛС/ИНН/ОГРН с контрольными суммами — из западных тулов такого нет ни у кого.

Запускаем: демо без реального провайдера

В репе лежит готовый quickstart с фейковым LLM, который отвечает эхом — самое то, чтобы увидеть обе стороны маскирования:

git clone https://github.com/cloud-ru-tech/guardrails-llm-filter
cd guardrails-llm-filter/examples/quickstart
docker compose up --build

Шлём запрос с email, картой и телефоном — как обычному OpenAI-совместимому API, только на :8080:

curl -sS http://localhost:8080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"demo","messages":[{"role":"user","content":"Мой email john.doe@example.com, карта 4111 1111 1111 1111, позвони на +7 916 123-45-67"}]}'

Клиенту возвращается ответ с оригиналами — как будто прокси и нет:

{"choices":[{"message":{"content":"You said: Мой email john.doe@example.com, карта 4111 1111 1111 1111, позвони на +7 916 123-45-67"}}]}

А теперь смотрим, что получила «модель» (логи mock-llm):

mock-llm received (as the LLM sees it):
  "Мой email <EMAIL_1>, карта <CREDIT_CARD_1>, позвони на <PHONE_RU_1>"

Накидал ещё запросов позлее — СНИЛС, ИНН, ключ AWS, DSN от Postgres:

"СНИЛС <SNILS_1>, ИНН <INN_ORG_1>, вот ключ sk-proj-abc123... и IP <IPV4_1>"
"Пиши на <EMAIL_1>, AWS ключ <AWS_ACCESS_TOKEN_1>"

Стриминг тоже проверил. Шлём с "stream": true — клиенту прилетают обычные SSE-чанки с оригиналом внутри:

data: {..."delta":{"content":"You said: "}...}
data: {..."delta":{"content":"Пиши на jane@corp.ru"}...}
data: [DONE]

А в логах mock-llm — "Пиши на <EMAIL_1>". Демаскирование отработало прямо по SSE-кадрам: модель оригинала так и не увидела.

⚠️Не всё ловится из коробки

Заметил глазастый? Фейковый OpenAI-ключ sk-proj-… в моём тесте прошёл к модели немаскированным — а вот номер счёта рядом поймало общее правило generic-long-token. Мораль: каталог из 265 правил широкий, но не всеядный. Перед продом прогони свои реальные форматы данных через песочницу (о ней ниже) и добей дырки кастомными правилами.


Веб-консоль: вшита прямо в бинарь

На :9080 живёт management-консоль — TypeScript/React SPA, запечённая в тот же Go-бинарь через go:embed. Отдельного веб-сервера нет, CORS нет, деплоить нечего. Вот дашборд после моих экспериментов:

Дашборд «Обзор»: сколько запросов замаскировано, какие типы данных и правила срабатывали

Видно всё, что я натворил: 5 замаскированных запросов, разбивка по типам данных, топ правил — от pii.email до моего кастомного acme_token.

Правила: 265 встроенных + свои

Страница «Правила детекции»: 267 правил с переключателями, фильтрами и импортом/экспортом

Каждое правило включается/выключается тумблером, есть импорт/экспорт и фильтры. Кастомное правило добавляется одним POST — я завёл детект внутреннего токена:

curl -X POST http://localhost:9080/v1/rules \
  -H 'Content-Type: application/json' \
  -d '{
    "rule_id": "acme_token",
    "name": "ACME internal token",
    "data_type": 6,
    "regex": "\\bacme-[0-9a-f]{8}\\b",
    "masking": {"placeholder": "ACME_TOKEN"}
  }'

И следующий же запрос с acme-deadbeef ушёл к модели как <ACME_TOKEN_1>. Без рестарта, без передеплоя.

То же самое можно сделать мышкой — форма создания правила умеет больше, чем голый regex:

Форма нового правила: RE2-регулярка, ключевые слова, валидаторы, энтропия, бан-лист и проверка на примере

Обрати внимание на поля — это фактически конспект того, как устроен движок детекции:

🔑Ключевые слова — регистронезависимый пре-фильтр: дорогая регулярка запускается, только если в тексте есть совпадение по словам. Так 265 правил и живут в микросекундах.
Валидаторы — контрольные суммы (Luhn, СНИЛС, ИНН, ОГРН, IBAN) отсекают то, что похоже на номер карты, но им не является. Отсюда precision 99.9%.
🎲Мин. энтропия и бан-лист — чтобы generic-правила для токенов не хватали слова типа xxxxxxxx или примеры из документации.
🎯Группы захвата — можно маскировать не всё совпадение, а только часть (пароль в DSN, а не весь URL).

А вкладка «Активный набор» показывает, что реально участвует в сканировании: встроенные ∪ пользовательские − отключённые, сгруппированные по типам данных. Плюс импорт/экспорт всего набора файлом — удобно таскать политику между инстансами.

Песочница: прогон через боевой путь

Самая полезная страница для отладки: вставляешь текст, и настоящий пайплайн сканирования показывает, что было бы замаскировано:

Песочница: слева тестовый текст с PII, справа — маскированный результат и сработавшие правила

Ниже — таблица подстановок: какой плейсхолдер, что за оригинал, какое правило сработало. Email, телефон, карта (Luhn сошёлся), СНИЛС (контрольная сумма сошлась), AWS-ключ и полный DSN Postgres с паролем:

Таблица подстановок: плейсхолдер → оригинал → правило

Аудит: кто, что и когда

С включённым аудитом (GUARDRAILS_AUDIT_ENABLED=true) каждая маскировка пишется в журнал. В детали записи видно сработавшие правила и маскированные тексты запроса/ответа — то есть ровно то, что реально увидел провайдер:

Деталь записи аудита: сработавшие правила, замены и маскированный запрос/ответ

Настройки: enforce → detect без рестарта

Глобальная политика инстанса: главный рубильник маскирования, режим и список сканируемых типов данных. Применяется сразу, без перезапуска:

Настройки: выключатель маскирования, режим Enforce/Detect, типы данных

Самое интересное тут — Detect, он же теневой режим. Я переключил его прямо в консоли и повторил запрос с email и картой. Результат:

  • mock-LLM получил оригинальный текст — трафик не тронут;
  • в аудите появилась запись с mode: detect и списком правил, которые сработали бы: pii.email, pii.fin.credit-card.

Отсюда и сценарий внедрения: вешаешь прокси в detect, неделю-другую смотришь в «Обзоре», что и сколько маскировалось бы, чинишь ложные срабатывания — и щёлкаешь в enforce одним PUT-запросом:

curl -X PUT http://localhost:9080/v1/settings \
  -H 'Content-Type: application/json' \
  -d '{"enabled":true,"data_types":[1,2,3,4,5,6],"mode":"enforce"}'

Мониторинг: живые счётчики без внешнего стека

Страница «Мониторинг» показывает агрегаты Prometheus-счётчиков самого сервиса — без Grafana и вообще без чего-либо внешнего: режим, хранилище, топология, версия, счётчики enforce/detect и задержки этапов mask/demask:

Мониторинг: состояние сервиса, счётчики за всё время, задержки этапов

Для взрослого сетапа есть голый Prometheus-эндпоинт на :9090/metrics (неймспейс extproc_guardrails) и готовый дашборд Grafana в репе:

extproc_guardrails_data_type_triggers_total{data_type="PERSONAL_DATA"} 5
extproc_guardrails_data_type_triggers_total{data_type="ACCESS_TOKENS"} 2
extproc_guardrails_demask_duration_seconds_bucket{le="0.001"} ...
⚠️Консоль — не наружу

Config API на :9080без аутентификации. Задумка такая: консоль живёт только во внутренней сети/кластере, никогда не публичный ingress. И ещё нюанс из чеклиста безопасности: data-plane понимает override-заголовок x-guardrails-data-types, которым можно ослабить маскирование для запроса — если перед прокси стоит недоверенный клиентский трафик, фронтовый шлюз должен этот заголовок вырезать. Не повторяйте мой docker compose up на VPS с публичным IP.


Что под капотом

⚙️Один Go-бинарь. Data-plane на :8080, config API + консоль на :9080, метрики Prometheus на :9090, gRPC-управление на :9000.
🧠Хранилище по вкусу: in-memory (дефолт, одна реплика), Redis или Postgres — для нескольких реплик. Для прохождения трафика хранилище вообще не нужно: карта замен живёт в памяти запроса.
☸️Деплой: multi-stage Dockerfile (distroless, non-root), kustomize-база для Kubernetes, пробы /healthz и /readyz. Несколько реплик — через общее хранилище, изменения политики разъезжаются тикерами за ~30 секунд.
📊Аудит-трейл: retention 24h по умолчанию, записи асинхронные и fail-open, опциональное шифрование чувствительного контента (AES-256-GCM).
🔌Родственник для Envoy: тот же движок есть в форме gRPC-сайдкара ext_proc — проект guardrails-llm-filter-extproc.

Конфигурация — переменные окружения с префиксом GUARDRAILS_, обычно хватает одной:

docker run --rm -p 8080:8080 -p 9080:9080 \
  -e GUARDRAILS_UPSTREAM_BASE_URL=https://api.openai.com \
  ghcr.io/cloud-ru-tech/guardrails-llm-filter:latest

А точно regex вывозит против ML?

Главный скепсис к regex-детекции: «ну это же прошлый век, сейчас все гоняют NER-модели». Авторы померили свой пайплайн на публичном датасете hivetrace/pii-bench (1810 примеров, 13 типов ПДн):

Подход F1 precision
guardrails-llm-filter (regex + чексуммы) 93.1 99.9
GLiNER Guard pipeline (модель + regex) 84.4
GLiNER Omni, чистая модель 72.8

Пуант в том, что сырые ML-модели проваливаются ровно на структурированном PII — ИНН, ОГРН, карты — и в продакшн-режиме добирают качество… теми же regex-правилами. А для свободного текста («меня зовут Иван Петров, живу на Ленина 5») наоборот — regex слепой, тут нужен NER типа Presidio. Для трафика агентов и приложений, где чувствительное — это в основном ключи, токены и документы с чексуммами, regex-подход честно выигрывает: быстрее, дешевле и предсказуемее.


Когда брать, когда нет

Мой вердикт

Брать, если гоняешь корпоративный трафик через внешнего LLM-провайдера и надо, чтобы PII и секреты не покидали контур — особенно с российскими документами, которые западные тулы не знают в принципе. Один бинарь, поменял base URL — и работает.

Не брать, если нужна защита от джейлбрейков и контроль тем разговора (это к NeMo Guardrails), полновесный AI-шлюз с роутингом и бюджетами (LiteLLM/Portkey) или ML-NER для свободного текста на куче языков (Presidio).

Отдельный кайф для агентных сетапов: агент сам тащит в контекст логи, тикеты, куски базы — и ты не контролируешь, что окажется в промпте. Прокси на пути к провайдеру — последний рубеж, который отработает независимо от того, насколько дырявый у тебя харнес.


TL;DR

1Что: открытый (Apache 2.0) reverse-proxy от Cloud.ru — маскирует PII/секреты по пути к LLM, возвращает оригиналы в ответе, включая SSE-стрим
2Как: ~265 regex-правил + контрольные суммы (Luhn, СНИЛС, ИНН, ОГРН), микросекунды на запрос, без ML на data-path, fail-open
3Фишки: веб-консоль в бинаре (обзор, правила, песочница, настройки, аудит, мониторинг), shadow-режим с переключением на лету, кастомные правила через API/UI, Prometheus + Grafana
4Но: каталог правил не всеядный (мой sk-proj-ключ прошёл к модели) — проверь свои форматы в песочнице; config API без аутентификации — только во внутренней сети

Запустить демо — три команды, полчаса вечера. А дальше сам решай, готов ли ты и дальше отправлять номера карт клиентов в чужие дата-центры. 🫡