-- ============================================================================= -- anon_masking_labels.sql — правила динамического маскирования ПДн (pg_anonymizer) -- ============================================================================= -- Назначение: закрыть блокер B2. На боевом liderra.ru расширение anon 3.0.13 -- установлено (22.05.2026), но НЕ задано ни одного правила маскирования -- (anon.pg_masking_rules → 0 строк), поэтому pg_dump выгружает персональные -- данные в открытом виде. Этот файл декларативно навешивает SECURITY LABEL на -- все колонки-носители ПДн. -- -- БЕЗОПАСНОСТЬ ПРИМЕНЕНИЯ НА ПРОД: -- * SECURITY LABEL ON COLUMN активирует маскирование ТОЛЬКО для ролей с меткой -- 'MASKED'. Роли приложения (crm_app_user, crm_app_admin, crm_supplier_worker, -- crm_readonly, crm_migrator) видят реальные данные без изменений — работа -- портала НЕ затрагивается. -- * Реальные данные в таблицах НЕ переписываются: это динамическое маскирование, -- не статическое anon.anonymize_database() (его на проде НЕ запускаем). -- * RLS не затрагивается: маскирование (anon) и tenant-изоляция (RLS) — -- независимые механизмы. Колонки tenant_id / id / ключи партиций -- (received_at, created_at) / любые FK-ключи НЕ маскируются. -- * FK-целостность: маскируются только не-ключевые колонки. Для UNIQUE-колонок -- (users.email) применяется детерминированная anon.partial_email (сохраняет -- уникальность по входу); для phone — anon.partial (сохраняет форму). -- -- ПРИМЕНЕНИЕ (на проде, под postgres): -- LOAD 'anon'; -- \i anon_masking_labels.sql -- ПРОВЕРКА: -- SELECT relname, attname, masking_function -- FROM anon.pg_masking_rules ORDER BY relname, attname; -- ИДЕМПОТЕНТНОСТЬ: повторный SECURITY LABEL перезаписывает прежний для той же -- колонки — файл можно применять повторно. -- -- NB по синтаксису anon 3.0.13: в `MASKED WITH VALUE …` долларовые кавычки -- ($$…$$) не принимаются — используются одинарные (внутри метки удваиваются). -- JSONB с NOT NULL (supplier_leads.raw_payload) маскируется редакцией -- '{}'::jsonb (не NULL — иначе нарушился бы NOT NULL при restore дампа); -- nullable JSONB/INET маскируются в NULL. -- ============================================================================= -- ── users ──────────────────────────────────────────────────────────────────── SECURITY LABEL FOR anon ON COLUMN users.email IS 'MASKED WITH FUNCTION anon.partial_email(email)'; SECURITY LABEL FOR anon ON COLUMN users.phone IS 'MASKED WITH FUNCTION anon.partial(phone,2,$$******$$,2)'; SECURITY LABEL FOR anon ON COLUMN users.first_name IS 'MASKED WITH FUNCTION anon.fake_first_name()'; SECURITY LABEL FOR anon ON COLUMN users.last_name IS 'MASKED WITH FUNCTION anon.fake_last_name()'; SECURITY LABEL FOR anon ON COLUMN users.totp_secret IS 'MASKED WITH VALUE NULL'; SECURITY LABEL FOR anon ON COLUMN users.avatar_path IS 'MASKED WITH VALUE NULL'; -- ── deals (партиционированная — метка на родителе наследуется партициями) ───── SECURITY LABEL FOR anon ON COLUMN deals.phone IS 'MASKED WITH FUNCTION anon.partial(phone,2,$$******$$,2)'; SECURITY LABEL FOR anon ON COLUMN deals.phones IS 'MASKED WITH VALUE NULL'; SECURITY LABEL FOR anon ON COLUMN deals.contact_name IS 'MASKED WITH FUNCTION anon.fake_last_name()'; SECURITY LABEL FOR anon ON COLUMN deals.comment IS 'MASKED WITH VALUE NULL'; -- ── supplier_leads (SaaS-уровень, без RLS) ─────────────────────────────────── SECURITY LABEL FOR anon ON COLUMN supplier_leads.phone IS 'MASKED WITH FUNCTION anon.partial(phone,2,$$******$$,2)'; SECURITY LABEL FOR anon ON COLUMN supplier_leads.raw_payload IS 'MASKED WITH FUNCTION pg_catalog.jsonb_build_object()'; -- NOT NULL → {} через функцию (anon не берёт ::jsonb-каст в VALUE) -- ── pd_subject_requests (обращения субъектов ПДн, без RLS) ─────────────────── SECURITY LABEL FOR anon ON COLUMN pd_subject_requests.subject_email IS 'MASKED WITH FUNCTION anon.partial_email(subject_email)'; SECURITY LABEL FOR anon ON COLUMN pd_subject_requests.subject_phone IS 'MASKED WITH FUNCTION anon.partial(subject_phone,2,$$******$$,2)'; SECURITY LABEL FOR anon ON COLUMN pd_subject_requests.subject_full_name IS 'MASKED WITH FUNCTION anon.fake_last_name()'; SECURITY LABEL FOR anon ON COLUMN pd_subject_requests.description IS 'MASKED WITH VALUE NULL'; SECURITY LABEL FOR anon ON COLUMN pd_subject_requests.response_text IS 'MASKED WITH VALUE NULL'; -- ── pd_processing_log (партиц.; прямой ПДн — только IP) ────────────────────── SECURITY LABEL FOR anon ON COLUMN pd_processing_log.ip_address IS 'MASKED WITH VALUE NULL'; -- ── auth_log (журнал входов) ───────────────────────────────────────────────── SECURITY LABEL FOR anon ON COLUMN auth_log.email IS 'MASKED WITH FUNCTION anon.partial_email(email)'; -- ── tenants (контактная почта арендатора) ──────────────────────────────────── SECURITY LABEL FOR anon ON COLUMN tenants.contact_email IS 'MASKED WITH FUNCTION anon.partial_email(contact_email)'; -- ============================================================================= -- КОНЕЦ. Перечень колонок — по контракту спеки -- docs/superpowers/specs/2026-06-17-anon-masking-labels-prod-spec.md (раздел D1). -- Рецепт маскированного дампа и план применения — в -- docs/security/pgaudit-anonymizer-setup.md. -- =============================================================================