Шифрование в PostgreSQL: почему спас только DuckDB

В проекте C&B-аналитики (зарплатные вилки, грейды, перцентили) стояло жесткое ограничение: оклады и совокупный доход нельзя держать в PostgreSQL в открытом виде. Данные должны оставаться нечитаемыми даже при полной утечке дампа базы или компрометации доступа к слою хранения.

Первое решение казалось очевидным — Client-Side шифрование на уровне приложения. Бэкенд упаковывал чувствительные атрибуты в AES-256-GCM (LargeBinary) прямо перед записью в базу.

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

1. SQL ослеп. По зашифрованным блобам невозможно сделать WHERE, ORDER BY, сгруппировать по диапазонам зарплат или посчитать перцентили силами СУБД. 2. На клиенте работает AG-Grid с серверной пагинацией (Server-Side Row Model) и десятками фильтров. Без открытых колонок серверная фильтрация в базе физически невозможна. 3. Вариант расшифровывать на лету в Python привел к катастрофе на бэкенде.

Особенность SSRM в том, что фронтенд шлет отдельный запрос к API на каждый скролл, сортировку или клик по фильтру. При нескольких активных аналитиках бэкенд на каждый HTTP-запрос был вынужден вычитывать десятки тысяч строк, плодить в памяти миллионы тяжелых Python-объектов, расшифровывать их в цикле и тут же отбрасывать 99% данных ради пачки в 50 строк. Аллокации памяти мультиплицировались на число параллельных запросов, GC не успевал освобождать кучу, и воркеры падали по OOM.

Решение: разделение на слепое хранилище и горячую песочницу

Вместо попыток заставить PostgreSQL выполнять аналитику по шифротексту, мы разделили зоны ответственности:

• PostgreSQL остался долгосрочным сейфом. Он хранит структуру, справочники и слепой зашифрованный шифротекст. • Встроенный DuckDB стал изолированной аналитической песочницей под сессию аналитика.

Как устроен пайплайн:

1. При открытии рабочей сессии срез данных один раз потоково расшифровывается и компактно упаковывается в колоночный формат через Polars/DuckDB. 2. Результат пишется в локальный файл с нативным шифрованием DuckDB (TDE): ATTACH ‘session_user_123.db’ AS session (ENCRYPTION_KEY ‘…’); Ключ шифрования генерируется под время жизни сессии. Данные на диске физически зашифрованы — при сбое пода или утечке дискового пространства прочитать файл невозможно. 3. Вся работа с памятью ушла из рантайма Python в C++ ядро DuckDB. Движок работает через собственный компактный буферный пул и читает с диска только те колонки и страницы, которые реально участвуют в запросе. На каждый чих AG-Grid (сортировки, фильтры, перцентили) DuckDB отдает в Python ровно запрошенные 20 строк в виде плоского Arrow-буфера без накладных расходов на создание ORM-моделей.

Второй выигрыш — локальный What-If калькулятор без воркеров Когда аналитик правит ставку, двигает ползунок регионального коэффициента или тестирует новую зарплатную вилку, ставить фоновые задачи в Celery/Taskiq бессмысленно — пользователю нужен мгновенный отклик в UI.

Все формулы (Compa-Ratio, дельты рыночных коэффициентов, МРОТ) мы перенесли прямо в DuckDB. Векторный SQL пересчитывает тысячи зависимых строк на лету внутри сессионной базы за миллисекунды.

В PostgreSQL изменения отправляются только тогда, когда аналитик осознанно нажимает «Сохранить утвержденную модель». Бэкенд заново шифрует измененные поля в AES-GCM и транзакционно коммитит их в основную базу.

Итог:

Не нужно заставлять транзакционную базу одинаково хорошо обеспечивать закрытое хранение и тяжелую аналитику. Достаточно разнести роли: слепое безопасное хранение — в PostgreSQL, а быстрые вычисления, фильтрация SSRM и моделирование — в изолированную зашифрованную песочницу на DuckDB, которая не раздувает память приложения.

Коллеги, сталкивались с требованиями ИБ по шифрованию аналитических полей? Как выкручивались: токенизация, гомоморфное шифрование или тоже выносили расчеты в отдельный слой?

Шифрование в PostgreSQL: почему спас только DuckDB | Сетка — социальная сеть от hh.ru