Приватность и 152-ФЗ
Gotcha — self-hosted-платформа: вы разворачиваете её на своей инфраструктуре и сами распоряжаетесь всеми данными. Это значит, что в терминах Федерального закона № 152-ФЗ «О персональных данных» оператором персональных данных (ПДн) становитесь вы, а не разработчики Gotcha. Эта страница помогает понять, какие ПДн обрабатывает система, что уже включено для минимизации, и какие обязанности остаются на вас.
Это техническая справка, а не юридическая консультация. За полной оценкой обязанностей обращайтесь к профильному юристу.
С dev-ключом шифрования at-rest нет вовсе
GOTCHA_SECRET_KEY — это то, чем шифруются client secret SSO, токены каналов и
значения HTTP-заголовков монитора (например, bearer-токен в Authorization) в
PostgreSQL. Пока он остаётся встроенным дефолтом для разработки, шифрование не
включается вообще: эти секреты лежат в базе открытым текстом. Шифровать ключом,
опубликованным в исходниках, было бы имитацией защиты, и продукт не делает вид,
что защита есть.
Два следствия, о которых стоит знать:
- Установка настоящего ключа при старте зашифрует и то, что уже сохранено: на каждом запуске приложение перешифровывает текущим ключом все читаемые секреты (client secret SSO, токены каналов доставки, заголовки мониторов), включая значения, всё ещё лежащие legacy-plaintext. Исключение — значения, которые уже нечитаемы (например, запечатаны потерянным ключом): их бэкфилл не трогает и пишет о них в лог; для таких секретов по-прежнему нужен ручной переввод через UI.
- При не-локальном
GOTCHA_BASE_URLприложение откажется стартовать с дефолтным ключом в режимахweb,all,ingestиuptime(везде, кромеprobe), поэтому обычно это касается только локальных инстансов — если отказ не был отключён явно.
Ротация ключа шифрования (GOTCHA_SECRET_KEY)
Прежде чем начинать: объявлять простой пользователям не нужно. Сессии
хранятся отдельно от шифрования at-rest — токен сессии не зависит от
GOTCHA_SECRET_KEY, поэтому ротация ключа никого не разлогинивает, а живые
сессии её переживают без следа. Естественное ожидание от смены ключа
шифрования обратное, отсюда и оговорка. Единственный, кого ротация может
задеть, — пользователь, оказавшийся в момент рестарта ровно посреди
OAuth-редиректа: подписанная cookie этого шага запечатана отдельным
подключом, который меняется вместе с GOTCHA_SECRET_KEY, и после рестарта
не пройдёт проверку — вход придётся начать заново. Тот же исход даёт любой
обычный рестарт, попавший в этот же короткий момент, так что для
пользователей ротация ничем не отличается от планового перезапуска.
Зашифрованные значения несут отпечаток ключа, которым они запечатаны
(enc:v2:<key-id>:...), поэтому ротация — управляемая, обратимая процедура,
а не разовая замена секрета:
Ротируйте, когда весь парк инстансов уже обновлён до версии с этой
процедурой. При rolling deploy старый бинарь рядом с новым не понимает
формат enc:v2:...: отдаёт его наружу как есть, будто это и есть секрет, и
пишет свежие значения мимо уже прошедшего бэкфилла, в устаревшем формате.
Начинайте ротацию только когда обновление раскатано на 100% парка.
Задайте
GOTCHA_SECRET_KEY_PREV=<старый ключ>и новыйGOTCHA_SECRET_KEY=<новый ключ>, перезапустите инстанс. Сразу на старте в лог пишется строка с id ключей кольца — она сообщает только состав кольца, а не итог перешифровки:INFO secretbox keyring ready current_key_id=<new-id> previous_key_id=<old-id> rotation_in_progress=truecurrent_key_id(<new-id>) — новый ключ, он понадобится на следующем шаге.previous_key_idвместе сrotation_in_progress=trueздесь только подтверждают, что кольцо действительно собралось с двумя ключами. Следом всё читаемое (client secret SSO, токены каналов, заголовки мониторов) перешифровывается новым ключом; настоящий итог — по одной строке на каждое из трёх хранилищ:INFO org: rewrap secrets backfill complete updated=<N> unreadable=<N> INFO alert: rewrap secrets backfill complete updated=<N> unreadable=<N> INFO uptime: rewrap secrets done updated=<N> unreadable_skipped=<N>Обратите внимание на разные имена поля: у uptime —
unreadable_skipped, у остальных двух —unreadable.unreadable/unreadable_skippedбольше нуля — не авария (секрет, потерявший оба ключа, иначе и не может выглядеть), но значит, что часть секретов нужно переввести вручную через UI, прежде чем убиратьPREV.Отдельно от этого счётчика в логе того же прохода могут быть строки уровня WARN. Три из них означают отказ на ОДНОЙ строке — по одному тексту на хранилище:
WARN alert: rewrap channel secret: update failed WARN org: rewrap sso secret: update failed WARN uptime: rewrap secrets: update monitor failedЭто секрет, который был читаем и должен был подняться на новый ключ, но точечный
UPDATEпо нему не прошёл из-за сбоя SQL. Такая строка НЕ входит ни вunreadable, ни вunreadable_skipped— по счётчику всё выглядит чисто, а секрет остался на старом ключе. (Есть и вторая причина непройденногоUPDATE— параллельная запись увела строку между чтением и апдейтом (CAS-промах); она вообще не логируется ни на каком уровне и отличить её от «уже всё поднято» можно только запросом ниже.)Бывает и отказ ЦЕЛОГО прохода по хранилищу —
rewrap org sso secrets failed,rewrap alert channel secrets failed,rewrap monitor header secrets failed. Он хуже построчного: хранилище не обработано вовсе, и итоговой INFO-строки по нему в логе не будет — то есть на шаге 1 вы недосчитаетесь одной из трёх.Если встретилась любая из этих строк — не переходите к шагу 3, а просто перезапустите инстанс ещё раз (шаг 1 повторно): бэкфилл идемпотентен, повторный проход подберёт то, что осталось.
Затем проверьте прямым запросом к БД, что не осталось ничего, что ещё не на новом ключе (
<new-id>— из строкиsecretbox keyring readyна шаге 1). Проверка версионно-нейтральна: она ловит не толькоv2под старым id, но и конверты САМОГО СТАРОГО формата —enc:<b64>без id ключа вовсе, в котором до первой ротации лежит вся база существующей инсталляции (в проверку поLIKE 'enc:v2:<old-id>:%'такой конверт не попал бы никогда):SELECT count(*) FROM alert_channels WHERE secret LIKE 'enc:%' AND secret NOT LIKE 'enc:v2:<new-id>:%'; SELECT count(*) FROM org_sso WHERE client_secret LIKE 'enc:%' AND client_secret NOT LIKE 'enc:v2:<new-id>:%'; SELECT count(*) FROM monitors WHERE kind = 'http' AND config::text ~ 'enc:(?!v2:<new-id>:)';Третий запрос — на regex с отрицательным предпросмотром (PostgreSQL поддерживает
(?!...)для оператора~по умолчанию).monitors.config— это JSON, где в одной строке может быть несколько значений заголовков сразу; обычныйNOT LIKE 'enc:v2:<new-id>:%', как у двух запросов выше, здесь бы не подошёл — строка с одним заголовком уже на новом ключе и другим ещё на старом дала бы ложное «чисто», потому что сравнивается вся строка целиком, а не каждое значение по отдельности. Запрос выше ловит именно такую строку: срабатывает, если внутри неё есть хотя бы одно вхождениеenc:, за которым не следуетv2:<new-id>:.Все три запроса должны вернуть
0.Уберите
GOTCHA_SECRET_KEY_PREVи перезапустите ещё раз. Кольцо снова состоит из одного ключа.
Ротация обратима, пока старый ключ не потерян: те же два ключа,
переставленные местами (GOTCHA_SECRET_KEY=<старый>,
GOTCHA_SECRET_KEY_PREV=<новый>), откатывают инстанс назад тем же проходом.
Бояться необратимости не нужно — именно этот страх обычно откладывает
ротацию.
Какие персональные данные обрабатываются
| Категория | Где хранится | Примеры |
|---|---|---|
| Аккаунт-холдеры Gotcha | PostgreSQL: users, user_identities | email при регистрации, email и subject от SSO/OAuth-провайдера |
| Конечные пользователи наблюдаемых приложений | ClickHouse: events, transactions, metric_points, logs | user_id, user_ip, user_email, атрибуты user.*/enduser.* в tags (transactions), attributes (metric_points), log_attributes (logs) |
| Свободный текст с возможными ПДн | ClickHouse: events.message/exception_value/stacktrace/contexts, spans.description/data, profile_samples.stack, logs.body, tags | всё, что SDK или разработчик поместил в текст ошибки, имя транзакции, SQL/URL, текст сообщения лога |
| Адреса доставки уведомлений | PostgreSQL: org_invites.email, конфигурация каналов | email приглашённых, адреса email/Telegram/webhook-получателей |
| Хостовая телеметрия | PostgreSQL: hosts (name, agent_version); ClickHouse: metric_points (метрики system.*) | host.name — часто внутреннее имя сервера, а не ПДн, но иногда содержит логин/домен владельца; версия установленного gotcha-agent; аптайм и системные метрики (CPU/память/диск/сеть) сами по себе персональных данных не несут |
Сессии (sessions) хранят только хеш токена и user_id, без IP и user-agent — минимизация соблюдена на уровне схемы.
Что уже включено по умолчанию
- Обезличивание IP и email действует и на ИМЕНА полей. При
GOTCHA_SCRUB_IP/GOTCHA_SCRUB_EMAILмаскируется значение любого поля, чьё имя (без учёта регистра и разделителей-_.) содержитemail— либо одно изuser_ip,ip_address,client_address,net_peer_ip,net_sock_peer_addr,network_peer_address,network_local_address,client_ip,x_forwarded_for,x_real_ip,remote_addr,cf_connecting_ip,Forwarded— в тегах, атрибутах OTLP,log_attributes,span.data, заголовках и query. Практическое следствие: тегcustomer_emailстанет[scrubbed], даже если его нет вGOTCHA_SCRUB_DENY_KEYS. Вернуть конкретное имя можно черезGOTCHA_SCRUB_KEEP_KEYS— он проверяется РАНЬШЕ правил по email и IP, то есть перекрывает их для точно совпавшего имени. Совпадение точное:GOTCHA_SCRUB_KEEP_KEYS=emailснимет маскирование только с поля, чьё нормализованное имя ровноemail, и не затронет ниcustomer_email, ниevents.user_email(тот зануляется отдельным правиломGOTCHA_SCRUB_EMAIL). - Обезличивание IP и email на приёме.
GOTCHA_SCRUB_IP=trueиGOTCHA_SCRUB_EMAIL=trueпо умолчанию:events.user_ipиevents.user_emailзануляются ещё до записи в ClickHouse. Ключи из денилиста (GOTCHA_SCRUB_DENY_KEYS) вычищаются из тегов, контекстов, заголовков, query-строк и тел запросов. Матч работает fail-closed: имя маскируется, если содержит слово из денилиста, поэтому ловятся иx_api_key, иclientSecret, иmytoken— а заодноauthor(содержитauth) иtokenizer(содержитtoken). Недо-маскирование — это утечка ПДн, а избыточное стоит лишь отладочного поля и обратимо черезGOTCHA_SCRUB_KEEP_KEYS. - Сроки хранения (retention). TTL применяется принудительно и настраивается: события и др. —
GOTCHA_EVENT_RETENTION_DAYS, спаны, метрики, профили, логи (GOTCHA_LOG_RETENTION_DAYS) — свои параметры (см. Конфигурация). Данные автоматически удаляются из ClickHouse по истечении срока (нулевой срок отключает удаление в своём классе). - Срок хранения распространяется и на сводные записи — каждая по сроку своей телеметрии. Группы ошибок и групп производительности (заголовок, culprit) удаляются, когда последнее их событие старше
GOTCHA_EVENT_RETENTION_DAYS; регрессии производительности — по тому же сроку. Регрессии профилей живутGOTCHA_PROFILE_RETENTION_DAYS, инциденты по метрикам —GOTCHA_METRIC_RETENTION_DAYS, закрытые инциденты аптайма —GOTCHA_INCIDENT_RETENTION_DAYS: сводная запись не должна переживать данные, которые она описывает, иначе карточка открывается пустой. Заголовок группы — свободный текст из вашего приложения, и переживать объявленный срок хранения он не должен. Открытые инциденты и регрессии не удаляются: они описывают то, что происходит сейчас. Нулевой срок отключает удаление в своём классе. - Обезличенные внешние уведомления. По умолчанию текст ошибки уходит только доверенным получателям — на ваши домены и во внутреннюю сеть; всем остальным, включая Telegram, отправляется обезличенная ссылка на инстанс. См. раздел о внешних получателях ниже.
- Нет «phone-home». Gotcha не отправляет никакой аналитики/телеметрии наружу к разработчикам. Единственные внешние получатели — те, что вы сами настроили (каналы оповещений, SSO-провайдеры).
- Защита от SSRF. Исходящие запросы (webhook-оповещения, аптайм-проверки) по умолчанию не ходят на приватные/loopback-адреса (
GOTCHA_SSRF_ALLOW_PRIVATE=false).
Права субъектов: доступ и удаление
152-ФЗ (ст. 14) и аналогичные нормы дают субъекту право на доступ к своим ПДн и их удаление. В Gotcha это реализовано на уровне организации (доступно роли owner):
- Выгрузка ПДн субъекта — экспортирует данные конечного пользователя (по
user_idили email), включая события, транзакции (в т.ч. по идентификаторам в тегах), метрики и логи (по идентификаторам вlog_attributes). - Удаление ПДн субъекта — удаляет те же данные из ClickHouse, а также спаны трейсов, чьи транзакции принадлежат субъекту (у таблицы спанов нет собственной колонки субъекта — она связывается с транзакцией по
trace_id; спан трейса без строки в транзакциях или трейса с несколькими участниками см. ниже).
Удаление проекта и организации. Строки в PostgreSQL снимаются каскадом сразу,
а телеметрия в ClickHouse ставится в очередь той же транзакцией и удаляется
фоновым исполнителем: восемь мутаций по многомесячным данным идут минутами, и
делать это внутри HTTP-запроса значило бы обрывать удаление по таймауту, оставляя
часть данных навсегда. Страница поэтому сообщает, что очистка поставлена в
очередь, а не выполнена. Проверить исполнение можно по метрикам
gotcha_purge_queue_depth и gotcha_purge_queue_oldest_seconds
(см. Самомониторинг): растущий возраст самой старой
заявки означает, что удаление не выполнено, и причина последней попытки записана
в самой заявке.
Важно: при включённом по умолчанию обезличивании поиск по email и IP не найдёт ничего.
GOTCHA_SCRUB_IPиGOTCHA_SCRUB_EMAILвключены по умолчанию, поэтомуevents.user_emailиevents.user_ipзануляются ещё на приёме — искать субъекта по ним уже не по чему. То же относится кtags/attributes/log_attributesв transactions, metric_points и logs: ключиuser.email/enduser.emailсодержат «email» и зануляются тем же правилом, поэтому субъект по email не находится и там. Работаетuser_id(и его аналоги в атрибутах —user.id/enduser.id): он намеренно исключён из обезличивания ровно ради этого права. Формы выгрузки и удаления предупреждают об этом на месте, а результат удаления показывается числом («удалено N записей» либо «записей не найдено»), чтобы исполнение требования можно было подтвердить.
Свободный текст (spans.data/description, profile_samples.stack, logs.body) программно по субъекту не удаляется — надёжно идентифицировать субъекта в произвольном JSON/стектрейсе/тексте сообщения нельзя. Эти поля обезличиваются истечением TTL (спаны — 30 дней, транзакции — 90, метрики — 30, профили — 7, логи — 14 по умолчанию); profile_samples.stack — сознательное решение оставить на TTL, а не удалять по субъекту, ровно по той же причине.
При этом строки спанов (не содержимое data/description, а сами записи) удаление субъекта всё же затрагивает — косвенно, через trace_id его транзакций. Остаются две границы: спан трейса, у которого уже нет строки в транзакциях (например, она вытеснена ретенцией раньше спанов), доживает до собственного TTL спанов; трейс с несколькими участниками (фоновая задача, межпользовательский запрос) удаляется целиком, если субъекту принадлежит хотя бы одна его транзакция, — спан не делится по субъектам внутри одного трейса.
Самоудаление аккаунта. Владелец аккаунта Gotcha может удалить свой аккаунт на странице профиля — каскадно удаляются привязанные способы входа, членство в организациях и сессии. Если он единственный владелец организации, сначала нужно передать владение или удалить организацию.
Обезличивание свободного текста
По умолчанию GOTCHA_SCRUB_FREETEXT=false: текст ошибок, стектрейсы и описания спанов пишутся дословно — кроме секретов внутри URL: query-параметры по денилисту, фрагмент и basic-auth вычищаются из URL ВСЕГДА, независимо от этого флага (наивная маскировка ломала бы SQL/URL и снижала пользу). Если разработчики могут поместить ПДн прямо в текст ошибки, включите GOTCHA_SCRUB_FREETEXT=true (маскирует email в свободном тексте) и дополнительно настройте scrubbing на стороне SDK.
Внешние получатели и трансграничная передача
Когда вы подключаете канал оповещений или SSO, ПДн могут покидать ваш периметр:
| Получатель | Что уходит | Юрисдикция |
|---|---|---|
| Telegram | ссылка оповещения и chat_id получателя; текст — только если канал отмечен как ваш собственный (или при GOTCHA_EXTERNAL_CHANNEL_DETAILS_ENABLED=true) | серверы за пределами РФ |
| Email (SMTP) | email получателя и ссылка оповещения; текст — только доверенным получателям (см. ниже) | ваш SMTP-сервер и почтовый сервер получателя |
| Webhook | payload оповещения; детали — только доверенным хостам (см. ниже) | адрес, который вы указали |
| OAuth/SSO (Yandex ID, VK ID, generic OIDC) | email/subject при входе | провайдер (Yandex/VK — РФ) |
Передача текста ошибок (с возможными ПДн) за пределы вашего контура — это потенциально трансграничная передача (152-ФЗ ст. 12). Поэтому по умолчанию детали события — заголовок, culprit, уровень, тело письма — уходят только доверенным получателям; остальным отправляется обезличенное уведомление со ссылкой на проблему в интерфейсе.
Доверенным считается получатель, чей адрес принадлежит вашей инфраструктуре:
| Получатель | Когда считается доверенным |
|---|---|
домен адреса — хост вашего инстанса (или его поддомен), либо перечислен в GOTCHA_TRUSTED_RECIPIENTS | |
| Webhook | хост URL — то же самое, а также любой адрес во внутренней сети: localhost, приватные диапазоны (10.0.0.0/8, 192.168.0.0/16, …), зоны .local, .internal, .lan, .home.arpa |
| Telegram | по адресу — никогда: получатель задан chat_id, домена у него нет, и подтвердить принадлежность к вашему контуру нечем |
| Любой канал | отметка «Получатель внутри моего контура» в форме канала — оператор заявляет то, чего адрес не показывает |
Решение принимается по получателю, а не по типу канала. Ящик на публичном почтовом сервисе — такая же чужая инфраструктура, как и Telegram; вебхук на ваш собственный сервер во внутренней сети не покидает контура вовсе.
Отметка на канале нужна там, где адрес ничего не доказывает. Типичный случай — Telegram: инстанс селфхостится, чат принадлежит самому оператору, и никакой разбор chat_id этого не покажет. До появления отметки выбор был между «нигде» и GOTCHA_EXTERNAL_CHANNEL_DETAILS_ENABLED=true, то есть «везде и всем». Отметка ставится по одному каналу, вручную, и по умолчанию снята; каналы с ней помечены в таблице бейджем «С деталями», чтобы состав получателей ПДн был виден с одного взгляда, а не выяснялся открытием каждого канала.
Ответственность за это заявление лежит на операторе: продукт проверить его не может — в том и причина, по которой отметка ставится руками.
Домен вашей организации, если он отличается от хоста инстанса, укажите явно:
GOTCHA_TRUSTED_RECIPIENTS=corp.example,ops.corp.example
Совпадение идёт по границе доменной метки: corp.example покрывает mail.corp.example, но не evilcorp.example. Родительский домен инстанса не доверяется автоматически: подъём на уровень вверх от gotcha.github.io выдал бы доверие всему github.io, то есть чужим проектам на том же хостинге.
GOTCHA_EXTERNAL_CHANNEL_DETAILS_ENABLED=true снимает ограничение целиком — детали пойдут любому получателю всех проектов, включая Telegram. Если задача в том, чтобы открыть детали одному-двум своим каналам, вернее отметить их в форме канала: результат тот же, а остальные получатели остаются под защитой.
Какая политика действует, видно в логе при старте: строка alert details: sent only to trusted recipients перечисляет хост инстанса и заданный список.
Ваши обязанности как оператора (152-ФЗ)
Ниже — ориентир, а не исчерпывающий список; сверяйтесь с актуальной редакцией закона и юристом:
- Локализация БД (ст. 18.5). Хранение ПДн граждан РФ должно вестись в базах данных на территории РФ. Gotcha не привязана к зарубежным хостингам — разворачивайте PostgreSQL и ClickHouse в нужной юрисдикции.
- Уведомление Роскомнадзора о намерении обрабатывать ПДн (за исключением случаев из ст. 22).
- Публикация политики обработки ПДн (ст. 18.1 ч. 2 п. 2). Используйте инвентарь выше как основу.
- Основания обработки и согласие субъектов, где это требуется.
- Меры защиты ПДн: доступ к инстансу, шифрование каналов (TLS), резервные копии, разграничение ролей.
Технические средства для соблюдения (обезличивание, retention, экспорт/удаление, обезличенные внешние уведомления) уже встроены — но их настройка и юридическая часть остаются на операторе.