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

4.8 KiB
Raw Permalink Blame History

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 на разных портах:

// 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 тесты гоняют оба контура

Альтернативы рассмотренные

  1. Один контур с строгой ролевой моделью — отвергнуто, ошибка в RBAC становится критичной уязвимостью
  2. Два разных процесса/контейнера — overkill для одной VM, осложняет shared state (token cache)
  3. API Gateway (Kong/Traefik) — лишняя сущность для проекта такого размера