Files
ZBrain/docs/DECISIONS/0003-postgres-isolation.md
zuevav f4bca8449e main
2026-05-20 19:33:02 +03:00

104 lines
5.4 KiB
Markdown
Raw Permalink 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.
# ADR-0003: Изоляция брейнов через отдельные PostgreSQL БД
**Дата:** 2026-05-20
**Статус:** Принято
## Контекст
ZBrain хостит несколько брейнов с разным уровнем чувствительности данных:
- **zetit** — внутренние процессы ZETIT, не публичные
- **telerapharma** — корпоративная инфраструктура с NDA
- **personal** — личные данные владельца
- **community** — данные о Смоленской 10, дом-чат, ЖКХ
Утечка MCP токена с правами на один брейн **не должна** давать доступ к другим брейнам.
Варианты изоляции в Postgres (от слабой к сильной):
1. **Одна БД, одна схема, отдельные таблицы с префиксами** — слабая изоляция, любой SQL injection даёт доступ ко всему
2. **Одна БД, отдельные schema'ы (zetit.pages, telerapharma.pages)** — изоляция через `search_path`, но один Postgres user видит всё
3. **Одна БД, отдельные пользователи с row-level security** — сложно, легко допустить ошибку в RLS политиках
4. **Отдельные БД с отдельными пользователями** — изоляция на уровне СУБД
## Решение
Вариант 4: **отдельная БД + отдельный Postgres-пользователь на каждый брейн**.
```
PostgreSQL cluster
├── brainhub (БД)
│ └── владелец: brainhub
│ └── таблицы: users, sessions, tokens, brains, sources, projects, audit_log
├── gbrain_zetit (БД)
│ └── владелец: gbrain_zetit
│ └── полная схема gbrain (pages, chunks, embeddings, links, ...)
├── gbrain_telerapharma (БД)
│ └── владелец: gbrain_telerapharma
│ └── ...
├── gbrain_personal (БД)
│ └── владелец: gbrain_personal
│ └── ...
└── gbrain_community (БД)
└── владелец: gbrain_community
└── ...
```
Каждый gbrain-instance подключается к Postgres под своим пользователем и **физически не видит** другие БД:
```sql
-- Под пользователем gbrain_zetit
\l
-- Покажет только gbrain_zetit (и template/postgres)
\c gbrain_telerapharma
-- ERROR: permission denied for database gbrain_telerapharma
```
Brainhub использует свою БД `brainhub` под пользователем `brainhub` (тоже не видит остальные).
## Реализация
В `scripts/create-brain.sh`:
```sql
CREATE USER gbrain_${BRAIN_NAME} WITH PASSWORD '${random_password}';
CREATE DATABASE gbrain_${BRAIN_NAME} OWNER gbrain_${BRAIN_NAME};
GRANT ALL PRIVILEGES ON DATABASE gbrain_${BRAIN_NAME} TO gbrain_${BRAIN_NAME};
-- НЕ выдаём этому юзеру ничего на other databases
```
Пароль для каждого gbrain-юзера генерируется случайно и хранится:
1. В `/var/lib/zbrain/brains/<name>/config.json` (chmod 600, owner zbrain)
2. В DATABASE_URL внутри systemd unit (Environment="DATABASE_URL=...")
`brainhub` НЕ хранит эти пароли в своей БД (они не нужны для admin-операций) — только знает на каком порту слушает gbrain instance.
## Последствия
### Плюсы
- Физическая изоляция через ACL Postgres
- SQL injection в одном брейне не даёт доступ к другим
- pg_dump каждого брейна тривиально по отдельности
- Удаление брейна = одна команда `DROP DATABASE gbrain_X`
- Разные параметры тюнинга на разные брейны (если потребуется)
### Минусы
- Нельзя сделать JOIN между брейнами в SQL (но это и не нужно — поиск через brainhub)
- Каждая БД имеет свои индексы, общая нагрузка на shared_buffers Postgres делится
- Backup делает дамп каждой БД отдельно (несложно)
### Митигация
- shared_buffers задан с запасом (2GB на 8GB RAM)
- HNSW индексы строятся в `maintenance_work_mem=512MB`, одного брейна за раз достаточно
## Альтернативы рассмотренные
1. **Row-level security в одной БД** — сложно, требует консистентного применения политик на каждой таблице gbrain (а у gbrain их десятки). Один пропущенный CREATE POLICY = утечка
2. **Отдельные Postgres-кластеры на каждый брейн** — overkill, 4× процессов, 4× shared_buffers
3. **Отдельные VM на каждый брейн** — экстремальная изоляция, не нужна на нашем масштабе