93 lines
4.8 KiB
Markdown
93 lines
4.8 KiB
Markdown
# 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)** — лишняя сущность для проекта такого размера
|