Files
ZBrain/docs/DECISIONS/0002-two-zone-deployment.md
zuevav f4bca8449e main
2026-05-20 19:33:02 +03:00

93 lines
4.8 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-0002: Two-zone deployment (внутренний + публичный контур)
**Дата:** 2026-05-20
**Статус:** Принято
## Контекст
ZBrain должен обслуживать:
1. **Внутренних пользователей** — owner (Алексей) и будущая команда работают через VPN/локальную сеть ZETIT. Им нужен полный UI: дашборд, управление брейнами, токенами, источниками, аудит.
2. **Внешних агентов** — 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 на разных портах:
```typescript
// 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:
```nginx
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 тесты гоняют оба контура
## Альтернативы рассмотренные
1. **Один контур с строгой ролевой моделью** — отвергнуто, ошибка в RBAC становится критичной уязвимостью
2. **Два разных процесса/контейнера** — overkill для одной VM, осложняет shared state (token cache)
3. **API Gateway (Kong/Traefik)** — лишняя сущность для проекта такого размера