Приватность и 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% парка.

  1. Задайте 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=true
    

    current_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.

  2. 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.

  3. Уберите GOTCHA_SECRET_KEY_PREV и перезапустите ещё раз. Кольцо снова состоит из одного ключа.

Ротация обратима, пока старый ключ не потерян: те же два ключа, переставленные местами (GOTCHA_SECRET_KEY=<старый>, GOTCHA_SECRET_KEY_PREV=<новый>), откатывают инстанс назад тем же проходом. Бояться необратимости не нужно — именно этот страх обычно откладывает ротацию.

Какие персональные данные обрабатываются

КатегорияГде хранитсяПримеры
Аккаунт-холдеры GotchaPostgreSQL: users, user_identitiesemail при регистрации, email и subject от SSO/OAuth-провайдера
Конечные пользователи наблюдаемых приложенийClickHouse: events, transactions, metric_points, logsuser_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-сервер и почтовый сервер получателя
Webhookpayload оповещения; детали — только доверенным хостам (см. ниже)адрес, который вы указали
OAuth/SSO (Yandex ID, VK ID, generic OIDC)email/subject при входепровайдер (Yandex/VK — РФ)

Передача текста ошибок (с возможными ПДн) за пределы вашего контура — это потенциально трансграничная передача (152-ФЗ ст. 12). Поэтому по умолчанию детали события — заголовок, culprit, уровень, тело письма — уходят только доверенным получателям; остальным отправляется обезличенное уведомление со ссылкой на проблему в интерфейсе.

Доверенным считается получатель, чей адрес принадлежит вашей инфраструктуре:

ПолучательКогда считается доверенным
Emailдомен адреса — хост вашего инстанса (или его поддомен), либо перечислен в 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, экспорт/удаление, обезличенные внешние уведомления) уже встроены — но их настройка и юридическая часть остаются на операторе.