This commit is contained in:
zuevav
2026-05-20 19:33:02 +03:00
commit f4bca8449e
30 changed files with 4152 additions and 0 deletions
@@ -0,0 +1,92 @@
# 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)** — лишняя сущность для проекта такого размера