Изначально идея была грубее: взять логовую строку, посчитать энтропию у подозрительных кусков и скрывать всё, что похоже на случайный секрет.
PII здесь — это personally identifiable information, то есть персонально идентифицируемая информация: email, телефон, адрес, паспортные данные, номера карт, токены доступа и другие значения, которые не должны свободно гулять по логам.
На бумаге звучало неплохо. Многие токены, ключи и сессионные строки действительно выглядят как шум:
x9VdQp2Mz_La77kPq0sk_live_51Nx...eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Но быстро выяснилось, что одной энтропией нормальный фильтр не собрать.
С одной стороны, есть значения с низкой энтропией, которые всё равно надо скрывать: password=123, token=dev, cvv=000. С другой — полно технических строк, которые выглядят случайными, но не являются секретами: идентификаторы трассировки, UUID, короткие хэши коммитов, идентификаторы запросов, куски путей. Если сделать порог ниже, фильтр начинает портить полезные логи. Если поднять порог, начинает пропускать слабые секреты.
После этого в PII-Shield появились регулярки, чувствительные ключи, список исключений и отдельные валидаторы вроде алгоритма Луна для номеров банковских карт.
Мне не нравился и сам момент, в котором мы обычно пытаемся лечить такую проблему. Часто PII чистят уже на уровне Fluentd, Logstash, SIEM или какого-нибудь большого логового конвейера. Это полезно, но поздно: данные уже покинули приложение, уже прошли через часть инфраструктуры, уже могли попасть в буферы, ретраи, временные файлы и чужие дашборды.
Так появился PII-Shield: небольшой open-source инструмент, который старается вырезать персональные данные и секреты из логов до того, как они ушли из pod.
Репозиторий PII-Shield на Github
Идея
Самая короткая версия:
приложение пишет лог | vPII-Shield читает сырой лог рядом с приложением | vнаружу уходит уже очищенная строка
То есть не «почистим где-то потом», а «не дадим сырому значению выйти наружу».
PII-Shield сейчас можно использовать несколькими способами:
-
консольная утилита или контейнер, который фильтрует стандартный ввод и вывод;
-
sidecar-контейнер в Kubernetes;
-
Kubernetes operator, который добавляет sidecar через проверяющий webhook при создании пода;
-
Helm-чарты для установки;
-
WASM SDK для Node.js и Python, если хочется встроить сканер прямо в процесс.
Основной Kubernetes-сценарий выглядит так: приложение пишет лог в файл на общий том, sidecar читает этот файл, прогоняет строки через сканер и пишет очищенный поток в стандартный вывод. Дальше его уже забирает обычный логовый сборщик.
┌──────────────────── pod ────────────────────┐│ ││ app container ││ │ ││ │ /var/log/app/output.log ││ v ││ общий emptyDir том ││ │ ││ v ││ pii-shield sidecar -> sanitized stdout ││ │└─────────────────────────────────────────────┘ │ v Loki / ELK / S3 / SIEM
Да, это не невидимый перехват всего подряд. Приложение должно писать в известный файл. Зато этот путь легко проверить, он не требует shell внутри sidecar image и не лезет в рантайм приложения.
Что именно считается чувствительным
У сканера нет одной кнопки «найти всё приватное». Он работает несколькими слоями.
Первый слой — ключи. Если строка содержит password=..., token=..., secret=..., api_key=..., значение рядом с таким ключом надо скрыть без долгих размышлений.
input: payment failed token=sk_live_51Nx...output: payment failed token=[HIDDEN:9b22c1]
Второй слой — пользовательские регулярки. У многих компаний есть свои внутренние идентификаторы: номера заявок, policy id, customer id, medical record number, номера дел, номера договоров. Такие штуки плохо угадывать по общим признакам. Лучше явно сказать:
export PII_CUSTOM_REGEX_LIST='[ {"pattern": "^MRN-[0-9]{8}$", "name": "MedicalRecord"}, {"pattern": "^CASE-[0-9]{4}-[0-9]{6}$", "name": "CaseNumber"}]'
Тут была отдельная неприятность: если пользователь добавит десять правил и на каждом токене гонять десять отдельных regexp, обработка каждой строки может стать заметно тяжелее. Поэтому правила сначала проверяются по одному при загрузке конфигурации, а затем собираются в один общий regexp через |. Каждое правило заворачивается в группу, чтобы после совпадения понять, какое имя поставить в метку. Это не гарантирует выигрыш на любом наборе правил, но убирает последовательный перебор regex на каждом токене и хорошо ложится на частый путь обработки в сканере.
Условно:
(^MRN-[0-9]{8}$)|(^CASE-[0-9]{4}-[0-9]{6}$)
В коде это хранится как CombinedCustomRegex. Отдельные скомпилированные правила тоже остаются в конфиге, но основной путь использует общий regexp.
Третий слой — энтропия. Многие секреты выглядят как бессмысленный набор символов: API keys, сессионные токены, случайные пароли. Для этого используется энтропия Шеннона. Если токен достаточно длинный и “слишком случайный”, scanner считает его подозрительным.
input: Authorization failed: x9VdQp2Mz_La77kPq0output: Authorization failed: [HIDDEN:3e12aa]
Но энтропия быстро начинает спорить с реальностью. Хэши коммитов, UUID, идентификаторы трассировки, идентификаторы запросов и пути в файловой системе тоже могут выглядеть подозрительно. Поэтому есть список исключений:
export PII_SAFE_REGEX_LIST='[ {"pattern": "^[a-f0-9]{7}$", "name": "GitShortSHA"}]'
Список исключений применяется раньше остальных правил. Если написать слишком широкую регулярку, можно случайно разрешить то, что надо было скрыть. Это не баг, это цена ручной настройки.
Четвертый слой — проверки для конкретных типов данных. Например, номера банковских карт проходят через алгоритм Луна. Это не просто regexp на 16 цифр: scanner ищет последовательности длиной от 13 до 19 цифр, проверяет границы, отбрасывает слишком однообразные наборы цифр и затем считает контрольную сумму.
Для снижения ложных срабатываний есть еще контекстная проверка. Число, которое проходит Луна, но встречается как TraceId=4556737586899855, не должно автоматически считаться картой. А строка вроде visa card 4556737586899855 provided уже выглядит как карточный контекст и будет скрыта.
Зачем хэш вместо просто [REDACTED]
Если заменить всё на [REDACTED], отладка становится слепой. Иногда нужно понять, что в десяти разных ошибках фигурировал один и тот же токен или один и тот же пользовательский идентификатор, но само значение видеть нельзя.
Поэтому PII-Shield заменяет найденное значение на короткую метку:
[HIDDEN:a1b2c3]
Метка считается через salt. При постоянном PII_SALT одинаковые исходные значения дают одинаковые метки, и QA/SRE могут сопоставлять события между логами. При случайном salt после рестарта корреляция ломается.
Для боевого запуска salt надо хранить как secret:
env: - name: PII_SALT valueFrom: secretKeyRef: name: pii-shield-secrets key: salt
И лучше включить проверку длины:
PII_REQUIRE_STRONG_SALT=true
Быстрая проверка без Kubernetes
Быстрее всего потрогать scanner через контейнер:
echo 'login failed email=ivan@example.com password=MySecretPass123!' \ | docker run -i --rm ghcr.io/pii-shield/pii-shield:2.1.0
Ожидаемый смысл вывода:
login failed email=[HIDDEN:...] password=[HIDDEN:...]
Точный хвост метки зависит от salt.
Kubernetes: operator и политика
Для Kubernetes есть operator. Он ставится через Helm:
helm repo add pii-shield https://pii-shield.github.io/pii-shield/helm repo updatehelm install pii-shield-operator pii-shield/pii-shield-operator \ -n operator-system \ --create-namespace
Дальше создается PiiPolicy:
apiVersion: core.pii-shield.io/v1alpha1kind: PiiPolicymetadata: name: strict-policy namespace: defaultspec: injectionMode: file logPath: /var/log/app/output.log failPolicy: open
И deployment помечается label/annotation:
apiVersion: apps/v1kind: Deploymentmetadata: name: billing-apispec: template: metadata: labels: pii-shield.io/inject: "true" annotations: pii-shield.io/policy: "strict-policy"
Webhook добавляет sidecar, том и нужные настройки. Приложение пишет в /var/log/app/output.log, sidecar читает этот файл и печатает очищенный поток.
В Helm-чарте заложены довольно скромные лимиты ресурсов: 30Mi памяти и 50m CPU для отдельного sidecar. Для своих нагрузок это всё равно надо мерить на собственных логах, особенно если много JSON и длинных строк.
Почему scanner написан без тяжелой зависимости на JSON parser
Логи часто приходят как JSON, но тащить каждую строку через обычный encoding/json при постоянной обработке потока не хотелось. Там, где нам нужно найти значения и сохранить структуру, scanner идет более узким путем: разбирает JSON-подобный поток достаточно аккуратно для редактирования, но без полного превращения строки в объектную модель.
Это не делает код красивее. Зато меньше аллокаций и меньше сюрпризов под нагрузкой.
В тестах есть отдельные проверки на:
-
вложенный JSON;
-
битые строки;
-
бинарный мусор;
-
multilingual logs;
-
false positives;
-
кастомные regex;
-
fuzz regression.
Для scanner-only бенчмарков:
go test -bench=. -benchmem ./pkg/scanner
Для end-to-end CLI throughput:
./benchmark/run_benchmarks.sh
В текущих заметках по проекту обычный Go-сканер на синтетическом корпусе был на уровне микросекунд на строку. Нейросетевой детектор PII на CPU, который я пробовал как дополнительную проверку, оказался примерно на три порядка медленнее. Поэтому модельный слой не должен работать на каждой строке по умолчанию. Если он появится, то только как явно включаемый режим для медицинских, юридических, клиентских чатов поддержки и похожих доменов.
Fail open или fail closed
В логовом фильтре неприятный выбор: если сканер споткнулся, что делать?
fail open — пропустить строку дальше. Логи продолжают течь, приложение и наблюдаемость не слепнут, но есть риск сырого значения.
fail closed — не выпускать исходную строку, а отдать маркер ошибки. Безопаснее для приватности, хуже для отладки.
В PII-Shield это настраивается:
PII_FAIL_POLICY=open
или:
PII_FAIL_POLICY=closed
По умолчанию выбран open, потому что потеря логов в бою часто превращается в отдельный инцидент. Для нагрузок с жесткими требованиями к соответствию правилам выбор может быть другим.
Где сейчас границы
Это место я специально не хочу прятать в конец README мелким шрифтом.
PII-Shield уже можно запускать и проверять, но не все режимы одинаково зрелые:
-
режим чтения файла с sidecar-контейнером — основной практичный путь;
-
прозрачная защита обычных Kubernetes
stdout/stderrлогов еще не закрыта как боевой режим; -
pipe mode меняет команду запуска у целевого контейнера, его надо проверять на каждой нагрузке;
-
Kubernetes operator находится в фазе стабилизации;
-
eBPF mode — исследовательское направление, не контроль для боевого compliance;
-
интеграция с Proxy-Wasm gateway и визуальная панель управления пока в плане.
Это скучная часть, но она экономит нервы. Инструмент для защиты приватности без честных ограничений быстро превращается в декоративный щит.
Что оказалось самым сложным
Не сам regex. И не Helm.
Самое сложное — не испортить нормальные логи.
Если scanner слишком нервный, он начинает скрывать идентификаторы трассировки, хэши коммитов, безобидные UUID и куски путей. Если слишком спокойный, пропускает слабые пароли и самодельные токены. Если сделать один глобальный порог, он плохо ложится на разные языки и разные домены. Если дать пользователю список исключений, он может сам прострелить себе ногу.
Поэтому проект постепенно двигается не к «одному умному детектору», а к набору понятных слоев:
-
базовые чувствительные ключи;
-
строгие пользовательские правила;
-
энтропия с настройками;
-
список исключений;
-
профили для разных предметных областей;
-
возможно, дополнительная модельная проверка там, где она правда окупает задержку.
То есть меньше веры в один умный молоток, больше проверяемых правил.
Для чего это может пригодиться
PII-Shield не заменяет нормальную дисциплину логирования. Разработчики всё равно не должны писать пароли в log.info(...) и похожие вызовы логгера. API всё равно должны проверять входные данные. Команда безопасности всё равно должна смотреть срок хранения, доступы и экспорт логов.
Но фильтр рядом с приложением закрывает неприятный класс ошибок: случайное попадание чувствительной строки в общий логовый поток.
Особенно это актуально там, где логи идут дальше в:
-
централизованные хранилища;
-
озеро данных;
-
инструменты для разбора инцидентов;
-
аналитика и отчетность;
-
конвейеры для LLM/RAG;
-
AI-агенты, которые читают трассы и цепочки доказательств.
Последний пункт для меня был отдельным триггером. Если PII попала в обучающий или оценочный набор данных, последствия уже другие: просто удалить строку из Loki уже будет недостаточно.
Что дальше
Ближайшие направления:
-
добить боевой сценарий для Kubernetes stdout/stderr;
-
стабилизировать жизненный цикл оператора;
-
усилить проверку релизов: контрольные суммы, digest образов, происхождение артефактов;
-
довести профили для разных предметных областей;
-
проверить выборочную модельную проверку без удара по задержке;
-
продолжить работу над Proxy-Wasm/eBPF только там, где это даст реальный выигрыш.
PII-Shield пока не претендует на роль универсального лекарства. Скорее это практичный слой защиты в месте, где обычно остается дырка: между приложением и логовой инфраструктурой.
Если из этой статьи хочется унести одну мысль, пусть она будет такой: чистить приватные данные надо как можно ближе к месту, где они появились. Всё, что случается дальше, уже дороже, мутнее и хуже проверяется.
ссылка на оригинал статьи https://habr.com/ru/articles/1045422/