4.8 KiB
4.8 KiB
ADR-0002: Two-zone deployment (внутренний + публичный контур)
Дата: 2026-05-20 Статус: Принято
Контекст
ZBrain должен обслуживать:
-
Внутренних пользователей — owner (Алексей) и будущая команда работают через VPN/локальную сеть ZETIT. Им нужен полный UI: дашборд, управление брейнами, токенами, источниками, аудит.
-
Внешних агентов — Claude Code на серверах клиентов (TeleraPharma, Fontvielle, и т.д.), которым нужен MCP-доступ к чтению/записи брейнов. У этих серверов нет VPN-доступа к ZETIT.
Простое решение "выставить всё на публичный домен с auth" имеет недостатки:
- Любая уязвимость в админ-UI становится публично доступной
- OAuth flow на админ-эндпоинтах усложняет admin интеграции (curl, скрипты)
- Утечка session cookie с админ-привилегиями через CSRF/XSS критична
Решение
Развести функционал по двум сетевым контурам с разными доменами:
Внутренний контур (brain.zetit.local)
- Доступен только из сети ZETIT и через VPN
- Plain HTTP допустим (доверенная сеть, нет MITM риска)
- Endpoints: всё —
/,/api/*,/mcp/*,/api/events(SSE) - Auth: session cookie через local login (email/password) или OAuth
- Используется: для web-UI, админских скриптов, debug'а
Публичный контур (brain.zetit.ru)
- Доступен из интернета через TLS
- Endpoints: только
/mcp/*,/oauth/*,/health - Auth: Bearer tokens (для MCP) или OAuth flow (для подключения нового клиента)
- Любой другой path → 404 (без подсказок что вообще существует)
- Rate limiting per IP и per token
- Используется: для удалённого Claude Code/Cursor, для машинного доступа
Реализация
Один Node.js процесс запускает два независимых Express app на разных портах:
// apps/api/src/main.ts
const internalApp = createInternalApp(); // полный набор routes
const publicApp = createPublicApp(); // только /mcp и /oauth
internalApp.listen(3000, '127.0.0.1');
publicApp.listen(3010, '127.0.0.1');
Nginx разруливает по hostname:
server {
server_name brain.zetit.local;
location / { proxy_pass http://127.0.0.1:3000; }
}
server {
server_name brain.zetit.ru;
listen 443 ssl;
location /mcp/ { proxy_pass http://127.0.0.1:3010; }
location /oauth/ { proxy_pass http://127.0.0.1:3010; }
location / { return 404; }
}
Последствия
Плюсы
- Атака на админ-UI требует VPN или физического доступа к сети
- Невозможно случайно выставить админ-API в публичный контур (architectural enforcement)
- Rate limit на публичном контуре более агрессивный, не мешает админам
- Раздельные access логи nginx для security audit
Минусы
- Сложнее тестировать: нужно эмулировать два контура локально
- DNS: две A-записи на разных серверах (внутренний и edge)
- Невозможно "быстро дать админский доступ" партнёру не давая VPN
Митигация для разработки
- Локально оба app слушают
127.0.0.1на разных портах, host header проверяется в middleware .env.developmentимеет флагDISABLE_ZONE_CHECK=trueдля упрощённого dev режима- Integration тесты гоняют оба контура
Альтернативы рассмотренные
- Один контур с строгой ролевой моделью — отвергнуто, ошибка в RBAC становится критичной уязвимостью
- Два разных процесса/контейнера — overkill для одной VM, осложняет shared state (token cache)
- API Gateway (Kong/Traefik) — лишняя сущность для проекта такого размера