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

5.4 KiB
Raw Permalink Blame History

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 под своим пользователем и физически не видит другие БД:

-- Под пользователем gbrain_zetit
\l
-- Покажет только gbrain_zetit (и template/postgres)

\c gbrain_telerapharma
-- ERROR: permission denied for database gbrain_telerapharma

Brainhub использует свою БД brainhub под пользователем brainhub (тоже не видит остальные).

Реализация

В scripts/create-brain.sh:

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 на каждый брейн — экстремальная изоляция, не нужна на нашем масштабе