Files
portal/docs/security/pgaudit-anonymizer-setup.md
T

189 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pg_audit (#28) + pg_anonymizer (#29) — установка на боевом сервере
**Статус:** ✅ установлены на боевом `liderra.ru` 22.05.2026. PostgreSQL 16.14, БД `liderra`.
> **Факт на 17.06.2026 (снятие расхождения).** Проверено `\dx` на проде по ssh:
> `anon 3.0.13` + `pgaudit 16.0` стоят. Прежний go-live аудит, отметивший «anon не
> установлен» (`pg_extension` → пусто), опрашивал **локальную dev-копию**, а не прод —
> на dev (native Windows PostgreSQL) расширений нет и быть не может (anon 3.x — Rust/pgrx,
> Windows-сборки нет; Docker/WSL невозможны — нет вложенной виртуализации). dev держит
> только демо-данные, реальных ПДн там нет.
>
> **Остаток B2 (закрывается этим документом).** На проде `anon` установлен, но
> `anon.pg_masking_rules` → **0 строк**: правил маскирования не задано, поэтому обычный
> `pg_dump` всё ещё выгружает ПДн открыто. Правила вынесены в `db/anon_masking_labels.sql`;
> рецепт маскированного дампа и план применения — в разделе «#29 …» ниже.
Это два расширения PostgreSQL фазы 3 (Compliance), которые нельзя было поставить на dev native-Windows PG (расширения там недоступны — см. `memory/project_phase1_strategy.md`). Внедрены, когда появился боевой Linux-сервер.
Сервер: VM `liderra-test` (Ubuntu 24.04), `ssh -i ~/.ssh/liderra_deploy ubuntu@111.88.246.137`, кластер `16/main` (порт 5432), приложение `/var/www/liderra/app`.
---
## Бэкап перед работами (обязательно)
```bash
sudo -u postgres pg_dump -Fc -d liderra > /home/ubuntu/backups/liderra-pre-<TS>.dump
```
Точка отката от 22.05.2026: `/home/ubuntu/backups/liderra-pre-pgaudit-anon-20260522-010441.dump` (custom format, 1170 объектов).
---
## #28 pg_audit — журнал аудита БД (152-ФЗ)
**Что даёт:** server-side журнал DDL / изменений прав / записей данных в дополнение к прикладным `auth_log`, `pd_processing_log`, `incidents_log`.
**Установка (выполнено):**
```bash
sudo apt-get install -y postgresql-16-pgaudit # из штатного репозитория Ubuntu
sudo -u postgres psql -c "ALTER SYSTEM SET shared_preload_libraries = 'pgaudit';"
sudo systemctl restart postgresql@16-main # ← единственный перезапуск (~2с простоя)
sudo -u postgres psql -d liderra -c "CREATE EXTENSION pgaudit;"
sudo -u postgres psql -c "ALTER SYSTEM SET pgaudit.log = 'ddl, role, write';"
sudo -u postgres psql -c "ALTER SYSTEM SET pgaudit.log_parameter = 'off';" # ПДн НЕ в логах
sudo -u postgres psql -c "ALTER SYSTEM SET pgaudit.log_catalog = 'off';"
sudo -u postgres psql -c "SELECT pg_reload_conf();"
```
**Важно:** `pgaudit.log_parameter = off` — значения SQL-параметров (телефоны/почты лидов) НЕ попадают в логи, иначе аудит сам стал бы утечкой ПДн.
**Где логи:** `/var/log/postgresql/postgresql-16-main.log`, строки вида `AUDIT: SESSION,...`.
**Проверка:** `CREATE TABLE _smoke(id int); INSERT INTO _smoke VALUES (1); DROP TABLE _smoke;` → в логе три строки `AUDIT: ... DDL/WRITE/DDL` с `<not logged>` вместо значений.
---
## #29 pg_anonymizer (anon) — маскирование ПДн в выгрузках
**Что даёт:** маскированные дампы базы (телефоны → `+7******XX`, почты → `iv***.ru`), чтобы реальные ПДн не попадали в dev/staging. Правило §5.1 правил Claude.
**Версия:** anon 3.0.13 — это **Rust/pgrx 0.18.0** расширение; готового пакета нет ни в Ubuntu, ни в PGDG → собрано из исходников.
**Сборка (выполнено, ~15 мин):**
```bash
# build-deps (после — удалены, см. ниже)
sudo apt-get install -y build-essential postgresql-server-dev-16 pkg-config git
# Rust toolchain (в ~/.cargo, ~/.rustup — после удалены)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
source "$HOME/.cargo/env"
cargo install cargo-pgrx --version 0.18.0 --locked # версия = pgrx из Cargo.lock
cargo pgrx init --pg16 /usr/lib/postgresql/16/bin/pg_config # системный PG, без скачивания
git clone --depth 1 https://gitlab.com/dalibo/postgresql_anonymizer.git /tmp/anon && cd /tmp/anon
make extension PG_CONFIG=/usr/lib/postgresql/16/bin/pg_config PGVER=pg16 # длинная компиляция
sudo make install PG_CONFIG=/usr/lib/postgresql/16/bin/pg_config PGVER=pg16 # просто cp (cargo root-у не нужен)
```
**Подключение (выполнено) — БЕЗ перезапуска:**
```bash
sudo -u postgres psql -d liderra -c "CREATE EXTENSION anon CASCADE;"
sudo -u postgres psql -d liderra -c "SELECT anon.init();"
```
**Загрузка ПО ТРЕБОВАНИЮ (важно для производительности):** anon НЕ подключён через `session_preload_libraries` на всю БД — иначе 9.6 МБ `anon.so` грузились бы при каждом подключении портала. В сессии маскирования библиотека загружается явно:
```sql
LOAD 'anon';
SELECT anon.partial('+79161234567', 2, '******', 2); -- → +7******67
```
### Правила маскирования (`SECURITY LABEL`) — добавлено 17.06.2026
До 17.06.2026 на проде был только `CREATE EXTENSION anon` + `anon.init()`, но **ни одного
правила маскирования** — поэтому `pg_dump` отдавал ПДн открыто. Правила вынесены в
репозиторий: [`db/anon_masking_labels.sql`](../../db/anon_masking_labels.sql). Файл
декларативно навешивает `SECURITY LABEL` на ПДн-колонки `users`, `deals`, `supplier_leads`,
`pd_subject_requests`, `pd_processing_log`, `auth_log`, `tenants`.
**✅ ПРИМЕНЕНО НА ПРОД 17.06.2026.** `anon.pg_masking_rules` = **20 правил**. Маскирование
проверено на живых данных через временную masked-роль (создана и удалена, данные не менялись):
`deals.phone``+7******01`, `contact_name` → подменённая фамилия, `users.email`
`cl******@li******.test`, `supplier_leads.phone``79******59`. `pg_dump` под masked-ролью
больше не отдаёт ПДн открыто — блокер B2 закрыт.
> **Квирк anon 3.0.13 (важно для будущих правил).** `MASKED WITH VALUE` **не принимает
> каст** `…::jsonb` / `…::inet` (ни с `$$…$$`, ни с одинарными кавычками) — ошибка
> «is not a valid expression». Поэтому: nullable JSONB/INET (`deals.phones`,
> `pd_processing_log.ip_address`) маскируются в `MASKED WITH VALUE NULL`; NOT-NULL JSONB
> (`supplier_leads.raw_payload`) — через `MASKED WITH FUNCTION pg_catalog.jsonb_build_object()`
> (возвращает `{}`). Долларовые кавычки `$$…$$` внутри `MASKED WITH FUNCTION` (аргумент
> `anon.partial`) работают нормально.
**Почему применение безопасно для боевого портала:**
- `SECURITY LABEL ON COLUMN` активирует маскирование **только** для ролей с меткой
`MASKED`. Роли приложения (`crm_app_user`, `crm_app_admin`, `crm_supplier_worker`,
`crm_readonly`, `crm_migrator`) продолжают видеть реальные данные — портал не затронут.
- Реальные данные **не переписываются** (динамическое маскирование, не
`anon.anonymize_database()` — его на проде не запускаем).
- RLS не затрагивается: tenant-изоляция и маскирование независимы; `tenant_id`, `id`,
ключи партиций и FK-ключи не маскируются → ни изоляция, ни целостность не нарушаются.
- `users.email` и прочие UNIQUE/контактные почты маскируются детерминированной
`anon.partial_email` (сохраняет уникальность по входу).
**ПЛАН ПРИМЕНЕНИЯ НА ПРОД (выполняет владелец, по бэкапу выше):**
```bash
# 0) бэкап (см. раздел «Бэкап перед работами»)
# 1) применить правила (под postgres; роли приложения не трогаются)
sudo -u postgres psql -d liderra -c "LOAD 'anon';" -f /var/www/liderra/db/anon_masking_labels.sql
# 2) проверить, что правила зарегистрированы
sudo -u postgres psql -d liderra -c "SELECT relname, attname, masking_function FROM anon.pg_masking_rules ORDER BY relname, attname;"
# 3) выделенная роль для маскированных дампов (BYPASSRLS — чтобы выгрузить все тенанты)
sudo -u postgres psql -d liderra -c "CREATE ROLE anon_dumper LOGIN PASSWORD '<секрет-из-Lockbox>' BYPASSRLS;"
sudo -u postgres psql -d liderra -c "GRANT pg_read_all_data TO anon_dumper;"
sudo -u postgres psql -d liderra -c "SECURITY LABEL FOR anon ON ROLE anon_dumper IS 'MASKED';"
```
**Сделать маскированный дамп** (anon боевые данные не меняет — только при явном
`anonymize_database()`, которого на проде не запускаем):
```bash
# дамп под masked-ролью с прозрачным динамическим маскированием:
PGOPTIONS="-c anon.transparent_dynamic_masking=on" \
pg_dump -U anon_dumper -h 127.0.0.1 -d liderra -Fc > /home/ubuntu/backups/liderra-masked.dump
# либо обёртка pg_dump_anon, если присутствует в сборке anon
```
**Критерий приёмки дампа:** восстановить в одноразовую БД и убедиться, что
`deals.phone` имеет вид `+7******XX`, `users.email` частично замаскированы,
`supplier_leads.raw_payload` = `{}`, реальных телефонов/почт нет.
> **dev:** на native Windows PostgreSQL расширение `anon` не ставится (см. блок «Факт на
> 17.06.2026» в шапке). Доказательство маскированного дампа выполняется на проде по плану
> выше; dev держит демо-данные без реальных ПДн.
**Файлы:** `anon.so` + `anon.control` в `/usr/lib/postgresql/16/lib/` и `/usr/share/postgresql/16/extension/` — это standalone-файлы, не принадлежат apt-пакету (сохраняются при autoremove). После **мажорного** апгрейда PostgreSQL расширение нужно пересобрать (re-clone + rebuild).
---
## ⚠️ Незапланированный апгрейд PG + закрепление версии
Установка `postgresql-server-dev-16` из PGDG потянула апгрейд боевого `postgresql-16` **16.13 → 16.14** (сборка PGDG) с авто-перезапуском кластера. Минорный патч — данные целы, портал здоров. Закреплено против повтора:
```bash
sudo mv /etc/apt/sources.list.d/pgdg.list /etc/apt/sources.list.d/pgdg.list.disabled
sudo apt-mark hold postgresql-16 postgresql-client-16 # в `dpkg -l` статус 'hi', не 'ii'
```
Для будущего патча PostgreSQL — `sudo apt-mark unhold postgresql-16 postgresql-client-16` осознанно.
## Очистка build-инструментов (выполнено)
```bash
rm -rf ~/.cargo ~/.rustup ~/.pgrx /tmp/anon # Rust + исходники (~2.2 ГБ)
sudo apt-get purge -y build-essential postgresql-server-dev-16 pkg-config
sudo apt-get autoremove --purge -y # gcc/llvm/clang orphans
```
Расширения (`pgaudit.so`, `anon.so`) — отдельные файлы, очистку build-тулчейна переживают.
---
## Серверный слой защиты — отдельно
WAF / anti-brute / DDoS / мониторинг / секреты / TLS-HSTS-CSP / бэкапы — это инфраструктура, не расширения БД. Открытые вопросы **SEC-1..SEC-7** (`docs/Открытые_вопросы_v8_3.md`), привязка к Б-1.