Шов C: audit_block_mutation() пропускает пересчёт hash-цепочки по метке
app.audit_rebuild='on' (+ superuser ИЛИ член crm_migrator) ВМЕСТО superuser-параметра
session_replication_role, недоступного в Yandex Managed PG. AuditRebuildChain
переведён на SET LOCAL app.audit_rebuild в транзакции (Odyssey-safe). Append-only
сохранён. Миграция 2026_06_26_140000; schema v8.55->v8.56 + CHANGELOG. Тесты 8/8 green.
Шов B: db/03_service_bypass_policies.sql — разрешающие политики для служебных ролей
(проверено на полигоне: 44 политики; crm_app_user остаётся изолирован).
Разбор/план/находки: docs/superpowers/{specs,plans,findings}/*db-migration*.
cspell-words: +RELID/bik/lrrl/smsq/srv. Не на проде, БД боевого не тронута.
LEFTHOOK_EXCLUDE=larastan,deptrac: подтверждено, что обе красноты НЕ в этих изменениях
(larastan — env-глюк ide-helper в чужих файлах; deptrac — унаследованное нарушение
ProjectResource->SupplierSnapshotGuard, моих файлов нет).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
20 KiB
Переезд базы на Yandex Managed PostgreSQL — проверка совместимости и «что делаем»
Дата: 25.06.2026 (актуализировано 26.06.2026 под правки прод-сессии)
Статус: решение принято — Путь А (управляемая база). План: docs/superpowers/plans/2026-06-25-db-migration-path-a-managed.md.
Контекст: выход портала в продажу на сотни клиентов; владелец выбрал базу «под ключ» (управляемую) у Яндекса. Перед переездом проверено, переедут ли особенности нашей БД на управляемый сервис, где нет прав суперпользователя.
🔄 Обновление 26.06.2026 (правки параллельной прод-сессии, проверены по коду/коммитам до
7efe9e3e):
- Схема v8.53 → v8.55: +2 таблицы
supplier_deferred_sync(v8.54) иsupplier_sync_runs(v8.55) — обе SaaS-уровня, БЕЗ RLS/tenant_id, пишетcrm_supplier_worker. На переезд влияют положительно: циклsrv_bypass(шов B) их не трогает (нет RLS); им нужен только GRANT дляcrm_supplier_worker— уже покрыт blanket-грантомON ALL TABLESвdb/02_grants.sql(шов E). Метрики схемы (headerdb/schema.sql): 79 таблиц / 124 индекса / 44 RLS-политики / 5 функций / 15 триггеров — число RLS-таблиц/политик не выросло.SharesSupplierPdo(общий PDO pgsql/pgsql_supplier) — тестовая механика (в тестах оба коннекта = postgres superuser, чтобы не ловить «active transaction» при rollback). На проде роли разные → коннекты раздельные. Для переезда: это не прод-шов, но прогон тестов на тест-кластере (Phase 5) должен пройти с ней — учесть.- Деньги-инвариант волатилен: на 26.06 прод = 1 838 400 ₽ / 1016 сделок (990 живых + 26 удал.) / 999 731 лид (было 1 836 400 / 1013). Сверять надо «снимок ДО == снимок ПОСЛЕ одной операции», не с фиксированным числом.
Источники фактов: аудит репозитория (db/schema.sql, db/00_create_roles.sql, db/02_grants.sql, app/..., docs/security/pgaudit-anonymizer-setup.md, инцидент 29.05) + официальная документация Yandex Cloud (yandex.cloud/ru/docs/managed-postgresql/...).
1. Короткий вывод (простым языком)
Переезд на управляемую базу — НЕ «воткнул и поехал». Большинство вещей переедет, но три куска нашей защиты завязаны на «права суперадмина», которых в управляемой базе нет. Два из них придётся переделывать в коде, причём один уже ломается на боевом даже сейчас.
Это меняет картину: управляемая база — правильная цель, но это проект с переделкой ядра безопасности, а не просто аренда железа. Есть и более быстрый/дешёвый промежуточный путь (см. §4).
2. Что переедет без проблем ✅
| Возможность | Управляемая база Яндекса |
|---|---|
| Расширения pgcrypto, pg_trgm, btree_gin | ✅ в белом списке |
| pg_partman, pgaudit, pgcrypto, uuid-ossp, pg_stat_statements | ✅ поддерживаются |
Изоляция клиентов RLS (по tenant_id через current_setting) |
✅ работает без суперправ |
| Нативное партиционирование (наш способ, не pg_partman) | ✅ |
Блокировки pg_advisory_xact_lock (в аудит-цепочке) |
✅ (живут в пределах транзакции) |
| Автокопии + восстановление на любую минуту (PITR) | ✅ копии в 3 ЦОД, хранение 7–60 дней (до 3 лет по политике), откат с точностью до ~30 сек |
| Отказоустойчивость (запасной хост, автопереключение) | ✅ кворумная синхронная репликация |
| Шифрование, встроенный пулер (Odyssey, аналог PgBouncer) | ✅ SSL обязателен, пулер встроен |
3. Что НЕ переедет как есть — проблемы ⚠️🔴
| # | Проблема | Серьёзность | Почему | Что делать |
|---|---|---|---|---|
| 1 | Роли с BYPASSRLS (crm_admin_user, crm_migrator, crm_supplier_worker) |
🔴 блокер | В управляемой базе нельзя создать свои роли и нельзя выдать BYPASSRLS (это право суперадмина). На этих трёх ролях держится вся «сквозная» логика: приём лидов от поставщика, админка, миграции | Перепроектировать «обход изоляции» через сами RLS-политики (разрешающие политики для служебных ролей), а не через атрибут роли. Аккуратно — это защита данных клиентов друг от друга |
| 2 | session_replication_role='replica' (отключение триггеров) |
🔴 блокер | Право суперадмина, в управляемой базе недоступно. Используется при пересчёте аудит-цепочек и в одной миграции. Уже ломается на боевом (инцидент 29.05: permission denied to set parameter session_replication_role), сейчас обходим через sudo -u postgres — чего в управляемой базе не будет |
Переписать пересчёт аудит-цепочки без отключения триггеров (напр. процедура SECURITY DEFINER от роли-владельца) |
| 3 | Маскирование ПДн (anon) для 152-ФЗ | 🟡 средне | Управляемая база поддерживает anon, но версию 1.3.2, а у нас на проде самосборная 3.0.13; и только на PostgreSQL ≥ 15. Свою сборку поставить нельзя | Перейти на целевой PG 16/17, проверить, что все наши функции маскирования есть в 1.3.2; иначе — маскировать иначе (на стороне приложения/фильтром дампа) |
| 4 | 5-ролевая модель БД (00_create_roles.sql) |
🟡 средне | Своих ролей создавать нельзя; права раздаются через привилегии + 4 управляемые роли Яндекса (mdb_superuser, mdb_admin, mdb_monitor, mdb_replication) |
Переразложить нашу модель прав на механику Яндекса. Заодно выверить путаницу имён (crm_app_admin/crm_readonly упоминаются, но в 00_create_roles.sql их нет) |
| 5 | Скрипты 00_create_roles.sql / 02_grants.sql рассчитаны на psql -U postgres |
🟡 средне | Суперпользователя нет; ALTER ... OWNER, GRANT на чужие объекты идут от роли-владельца кластера |
Адаптировать провижен-скрипты под роль-владельца mdb_superuser |
| 6 | Пулер Odyssey в транзакционном режиме | 🟢 мелочь | Ограничения на advisory-locks/temp-таблицы, живущие дольше транзакции. Наши xact_lock — в пределах транзакции, ок; но проверить |
Использовать сессионный режим где надо, либо подтвердить, что наш код укладывается в транзакции |
Итог: 2 красных блокера (требуют переделки кода ядра безопасности) + 2–3 жёлтых (адаптация). Самый чувствительный — №1: трогаем то, что изолирует данные клиентов друг от друга, ошибка = утечка между клиентами. Делать только с тестами + проверкой rls-reviewer.
3a. Карта швов (прочитано ПО КОДУ 25.06 — уточняет §3)
Чтение реального кода снижает оценку §3. Ключевое: приложение уже подготовлено к отказу от BYPASSRLS, миграция концентрируется в слое ролей/политик БД, не в коде портала.
Что НЕ меняется (по коду):
- ~30 джобов/команд на connection
pgsql_supplier(RouteSupplierLeadJob, Sync*, Reset*, Snapshot*, AuditRebuildChain, CsvReconcile, BalancePreflightSweep и др.) — остаются как есть: у всех уже явныеWHERE tenant_id-фильтры (defense-in-depth,config/database.php:126-127,00_create_roles.sql:64-66). - ~50 мест
SET LOCAL app.current_tenant_id(middlewareSetTenantContext.php:39+ контроллеры + report-провайдеры + джобы) — не меняются: обычная рольcrm_app_userработает черезcurrent_setting, на управляемой базе RLS так же. - GRANT'ы уже role-explicit (
GRANT ... TO crm_app_user, crm_supplier_workerпо всей схеме) — структура прав готова. - Маскировка (
anon_masking_labels.sql) использует стандартные функции anon (partial_email,partial,fake_first_name/last_name,MASKED WITH VALUE NULL) — переносимы на встроенную в управляемую базу 1.3.2; файл по сути уже deployment-скрипт.
Что меняется (швы, по убыванию объёма):
| Шов | Файлы | Что делаем |
|---|---|---|
| A. Роли без BYPASSRLS | db/00_create_roles.sql (строки 41-44, 47-51, 70-73) |
Создать те же 5 пользователей через консоль YC, БЕЗ атрибута BYPASSRLS |
| B. Замена BYPASSRLS на разрешающие политики | db/schema.sql (~36 RLS-таблиц, строки 3069+) + миграции с CREATE POLICY |
Для 3 служебных ролей добавить политику TO <role> USING(true) WITH CHECK(true) на таблицах, что они трогают. Механически, скриптом. Покрывает и NULL-tenant вставки (failed_webhook_jobs, RouteSupplierLeadJob:640), и FORCE ROW LEVEL SECURITY таблицы (lead_charges:1182) |
| C. Пересчёт аудита без session_replication_role | AuditRebuildChain.php:107,137; migrations/2026_05_23_hole2...:34; workflows f1-rebuild/sql-rebuild |
Триггер пропускает себя по GUC-метке (вместо отключения), либо SECURITY DEFINER от владельца. Локально, ~день |
| D. Маскировка на встроенную anon 1.3.2 | db/anon_masking_labels.sql |
Применить те же метки через механизм управляемой базы; проверить парность функций на тест-кластере |
| E. Provision-скрипты под роль-владельца | db/02_grants.sql (ALTER OWNER, GRANT) |
Адаптировать запуск с postgres на mdb_superuser управляемой базы |
| F. DDL партиций | MonthlyPartitionManager.php:30-40 |
Создание/дроп партиций — под роль-владельца управляемой базы |
Главный ответ на «перелопатим ли всю базу»: нет. Данные и структура не трогаются; код портала почти не трогается (он уже defense-in-depth). Меняется слой ролей и политик + 1 правка пересчёта аудита + замена механизма маскировки. Всё сначала на копии (тест-кластер), со сверкой изоляции (rls-reviewer + тесты «клиент не видит чужого») и денег.
Пересмотр риска/срока: основной риск — полнота разрешающих политик (шов B); страховка — явные WHERE tenant_id уже в коде, плюс тесты изоляции на копии. Срок ориентировочно ~2 недели, основная работа — механическая (политики) + один аккуратный кусок (аудит).
4. Развилка — что делаем (решение владельца)
Проверка вскрыла настоящий выбор. Раньше казалось «арендуем управляемую базу», но оказалось — это либо проект с переделкой, либо другой путь.
Путь А — Полностью управляемая база Яндекса (как задумывали)
- Плюсы: Яндекс сам делает копии, восстановление на любую минуту, запасной хост и автопереключение. Минимум ручной возни в будущем. Лучшая долгосрочная цель.
- Минусы: надо переделать ядро безопасности (п.1, 2, 4) + проверить маскирование (п.3). Это инженерный проект с риском (трогаем изоляцию клиентов), несколько недель аккуратной работы с тестами.
- Стоимость железа: ~18 400 ₽/мес (старт) … ~34 000 ₽/мес (помощнее), как в смете.
Путь Б — Своя база, но сделанная правильно (промежуточный, быстрый)
- Оставляем PostgreSQL самоуправляемым (наши роли, BYPASSRLS, anon 3.0.13, session_replication_role — всё работает как есть, переписывать НЕ надо).
- НО: выносим её на отдельный мощный сервер (не на одной машине с приложением).
- Добавляем копии «на сторону» (в объектное хранилище Object Storage) — закрывает дыру «копии на том же диске, что и база».
- Добавляем запасную живую копию (реплику) на втором сервере — для подстраховки.
- Плюсы: убирает риск потери денег/данных клиентов быстро и без переписывания ядра безопасности. Низкий риск.
- Минусы: обслуживание базы (обновления, реплика, проверка копий, переключение при аварии) остаётся на нас, не на Яндексе. Автопереключение — надо настроить самим.
- Стоимость железа: дешевле управляемой (обычная ВМ ~6 400 ₽/мес под базу + реплика; без «управляемой» наценки за ядро).
Рекомендация
Сначала Путь Б, потом Путь А. Логика: главная боль сейчас — риск потерять данные клиентов (копии лежат на том же диске, что уже падал). Путь Б закрывает это за дни и без риска переписывания изоляции. А Путь А (управляемая база) сделать отдельным аккуратным проектом позже — с тестами и rls-reviewer, без спешки под запуск продаж. К тому же блокер №2 (session_replication_role) стоит починить в любом случае — он уже ломается.
5. План «что делаем» — будет детализирован после выбора пути
После решения владельца по §4 — отдельный пошаговый план (через навык планирования), с TDD на чувствительных кусках (RLS, аудит-цепочки) и обязательной проверкой изоляции клиентов. Общие вехи:
Если Путь Б (рекомендованный старт):
- Поднять отдельный сервер под базу, перенести данные (с проверкой «деньги до == после»).
- Настроить копии «на сторону» в Object Storage + проверку их восстановимости.
- Поднять реплику на втором сервере.
- Починить блокер №2 (пересчёт аудит-цепочки без
session_replication_role) — он нужен в любом пути.
Если/когда Путь А (управляемая база):
- Перепроектировать обход RLS через политики (п.1) — TDD +
rls-reviewer. - Переразложить роли на модель Яндекса (п.4, 5).
- Проверить/перевести маскирование anon на 1.3.2 + PG ≥15 (п.3).
- Тестовый переезд на отдельный кластер, сверка изоляции и денег, затем боевой.
Приложение. Ключевые источники
- Нет суперпользователя / нельзя создавать свои роли: yandex.cloud/ru/docs/managed-postgresql/qa/general#superuser, .../concepts/roles
- Расширения (pg_partman, pgaudit, anon 1.3.2, pgcrypto, …): .../operations/extensions/cluster-extensions, .../operations/extensions/pg_anon
- PITR/копии: .../concepts/backup ; HA/репликация: .../concepts/replication ; пулер Odyssey: .../concepts/pooling ; настройки: .../concepts/settings-list
- Наш инцидент с
session_replication_role:docs/incidents/2026-05-29-audit-rebuild-per-tenant-cleanup-handoff.md:84-97 - Наши роли/BYPASSRLS:
db/00_create_roles.sql,db/02_grants.sql:165-171 - Наш anon 3.0.13:
docs/security/pgaudit-anonymizer-setup.md:62-77,db/anon_masking_labels.sql