# Этап 2 — валидация на БОЕВОМ управляемом кластере (Yandex Managed PG)

**Дата:** 26.06.2026. **Кластер:** `c9q2cvtjpq3hgq6l0r96` (HA, PG 16.14), DB `liderra`, доступ через бастион `ssh liderra-prod`, пулер Odyssey порт 6432, SSL verify-full.
**Контекст:** продолжение [Путь А](../../../../«память»). Этап 1 (локальный полигон) — см. `etap1-sandbox-results.md`. Здесь — то же на РЕАЛЬНОЙ управляемой базе под `mdb_admin` (БЕЗ superuser, БЕЗ BYPASSRLS).

## Итог: модель безопасности и 152-ФЗ маскировка полностью работают на Managed PG. ✅

## 1. anon 1.3.2 (152-ФЗ) — ВКЛЮЧЁН и ПРОВЕРЕН ✅
Yandex Managed PG **официально поддерживает** `postgresql_anonymizer` (док: yandex.cloud/docs/managed-postgresql/operations/extensions/pg_anon). Версия на PG16 — **1.3.2** (методы: static masking + generalization; dynamic masking / anon-dumps на Managed НЕ предоставляются — для нашего кейса «маскированные копии» хватает static).

Процедура включения (3 шага, всё через control plane — SQL `CREATE EXTENSION` на Managed запрещён):
1. Загрузить shared-библиотеку `anon`: `yc managed-postgresql cluster update-config <id> --set shared_preload_libraries=anon` → **рестарт мастера** (на пустом кластере безвреден; health кратко DEGRADED при failover, затем RUNNING/ALIVE).
2. Добавить расширение в БД (⚠️ перечислять ВСЕ существующие, иначе пропущенные отключатся):
   `yc managed-postgresql database update liderra --cluster-id <id> --extensions btree_gin,pg_trgm,pgcrypto,anon`
3. Выдать роль `mdb_admin` владельцу БД (`crm_migrator`): `yc ... user update crm_migrator --grants mdb_admin`.

Проверка вживую: `anon 1.3.2.1` установлен; `anon.partial_email('ivan.petrov@example.com')` → `iv******@ex******.com`; полный static-masking демо (`SECURITY LABEL ... anon.fake_company()/random_zip()` + `anon.anonymize_database()`) → `Bank of Saratov`→`Carey PLC`, `405-657`→`92335`. ✅

## 2. schema.sql (v8.56) под mdb_admin — применяется почти идеально ✅
`psql -f schema.sql` под `crm_migrator` (mdb_admin): **90 таблиц / 44 RLS-политики / 159 функций**, всего **1 ошибка**.
- Единственная ошибка: `unrecognized configuration parameter "app.current_tenant_id"` на `ALTER TABLE lead_charges ADD CONSTRAINT lead_charges_deals_fk ... REFERENCES deals`. Причина: FK валидируется против `deals` (FORCE RLS); без `srv_bypass`-политики для `crm_migrator` (03 ещё не применён) политика `current_setting('app.current_tenant_id')::bigint` вычисляется без tenant-контекста → ошибка. На старом self-managed prod `crm_migrator` имел BYPASSRLS и не упирался.
- **Это артефакт СВЕЖЕГО schema.sql-лоада, НЕ блокер переезда:** реальный переезд идёт через `pg_dump`/`pg_restore` с прода, где FK уже создан и провалидирован. После применения `03_service_bypass` FK добавился штатно (rc=0).
- `CREATE EXTENSION IF NOT EXISTS pgcrypto/pg_trgm/btree_gin` — безопасные no-op (расширения уже включены через control plane).
- Живых `SET session_replication_role` и `OWNER TO postgres` в schema.sql НЕТ (шов C закрыт, ownership → crm_migrator).

## 3. 02_grants.sql — найден и исправлен 1 шов несовместимости с Managed ⚠️→✅
- **Падало:** `GRANT crm_migrator TO crm_supplier_worker WITH INHERIT TRUE` → `permission denied to grant role "crm_migrator"` (даже mdb_admin не имеет ADMIN OPTION на кастомную роль).
- **Решение (применено):** на Managed членство выдаётся через control plane — `yc ... user update crm_supplier_worker --grants crm_migrator` (выполнено, `pg_has_role(...,'MEMBER')=t`). Сам `02_grants.sql` обёрнут в `DO ... EXCEPTION WHEN insufficient_privilege` (NOTICE) → теперь применяется с **0 ошибок** на обеих средах (self-managed prod проходит штатно как superuser).
- Остальное (GRANT/REVOKE на владеемые таблицы, ALTER OWNER → crm_migrator) — применяется чисто под mdb_admin.

## 4. 03_service_bypass_policies.sql — чисто, 44 srv_bypass политики ✅
Идемпотентный DO-loop создал 44 `srv_bypass` (PERMISSIVE FOR ALL TO crm_admin_user/crm_migrator/crm_supplier_worker USING true). 0 ошибок (NOTICE'ы про DROP POLICY IF EXISTS — норма).

## 5. Изоляция арендаторов — проверена на РЕАЛЬНОЙ таблице `tenants` ✅
| Проверка | Результат | Ожидалось |
|---|---|---|
| crm_app_user ctx=A | видит только `isotest-a` | ✓ |
| crm_app_user ctx=B | видит только `isotest-b` | ✓ |
| crm_app_user без контекста | `(none)` | ✓ |
| crm_app_user cross-tenant UPDATE чужого | 0 строк | ✓ |
| crm_supplier_worker (srv_bypass) | видит обе (2) | ✓ |

## Состояние кластера после этапа 2
- DB `liderra`: полная схема v8.56 загружена (90/44/159) + 44 srv_bypass + FK lead_charges_deals_fk + anon. Тест-данные удалены. **Это валидированное «готово к переезду» состояние** (реальный переезд начнётся с чистки: `DROP SCHEMA public CASCADE; CREATE SCHEMA public;` затем `pg_restore` полного дампа прода — или схема уже стоит и грузим data-only; решение — на этапе переезда).
- Роли: 5 crm_* без BYPASSRLS; crm_migrator=mdb_admin (владелец БД); crm_supplier_worker ∈ crm_migrator (через yc). Пароли crm_migrator/crm_app_user/crm_supplier_worker сброшены на известные значения (session scratch: `mig-pw.txt`/`app-pw.txt`/`sup-pw.txt`) — **переложить в Lockbox перед боевым переездом**.

## Что осталось до боевого переезда (GATED — отдельный заход, с «давай» владельца)
prod-deploy-validator GO + тихое окно → дамп прода → restore в кластер → **сверка денег ДО==ПОСЛЕ** → переключить `app/.env` (DB_HOST мастер, DB_PORT=6432, DB_SSLMODE=verify-full, DB_SSLROOTCERT=CA, DB_SUPPLIER_USERNAME=crm_supplier_worker) → config:cache (www-data, квирк 107) → рестарт → verify HTTP 200 + изоляция + деньги. Старую базу держать ≥7 дней на откат. Перед дампом — выдать членство роли через yc (см. §3) и проверить anon включён.
