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