# Хаб: централизованное управление серверами Technitium без кластера Общий план. Внутри два пласта: - **общее управление**: каждый сервер из одной консоли, действия «на все серверы», сравнение и выравнивание; - **упор на Apps**: приложения на всех серверах и синхронизация их конфигов. Статус: план, ничего из этого ещё не сделано. Сейчас хаб только читает (`docs/hub-deploy.md`). --- ## 1. Контекст и цель - Есть два независимых сервера Technitium DNS: `mdns.shnt.top` и `ndns.shnt.top`. Кластера нет и не будет: серверы должны оставаться самостоятельными. - Хаб на `dnss.shnt.top` уже показывает статистику обоих: Dashboard, Logs, About, переключатель «All servers / mdns / ndns», предупреждение об отказе сервера. - **Цель:** управлять обоими серверами из хаба. Прежде всего приложениями (Apps): ставить, обновлять и держать их конфиги одинаковыми там, где нужно, не трогая того, что у каждого сервера своё. --- ## 2. Принципы 1. **Два режима записи.** - **На один сервер:** действие пересылается выбранному серверу (`node=mdns` или `node=ndns`). Так уже работает чтение. - **На все серверы:** хаб выполняет действие на каждом выбранном сервере и показывает итог по каждому отдельно. Тихого «ок» при частичном успехе не бывает. 2. **Чтение и запись разделены.** - Токены `hub-reader` (только View) остаются для чтения. - Для записи на каждом сервере заводится отдельный `hub-admin` только с теми правами, которые нужны этапу. 3. **Хаб защищается до того, как получает запись.** Роли, ограничение попыток входа, журнал действий (этап 0). 4. **Конфиги синхронизируются, данные нет.** - Синхронизируются тексты конфигов приложений и настройки. - Базы логов, кеш и статистика у каждого сервера свои. 5. **Общая часть плюс локальные поля.** - В конфигах приложений есть поля, свои для каждого сервера: сети Split Horizon, путь к базе Query Logs, локальные группы клиентов Advanced Blocking. - Синхронизируется только общая часть. Локальные поля задаются по каждому серверу. 6. **Источник правды для конфигов: эталон в хабе.** - Хаб хранит желаемый конфиг в своём томе (`/data`), серверы приводятся к нему. - Первый эталон берётся импортом с любого сервера. - Альтернатива: ведущий сервер (mdns) как эталон. Она проще, но хуже: зависит от живучести одного сервера и не даёт истории. 7. **Зоны DNS лучше синхронизировать средствами Technitium.** Primary-зона на одном сервере и secondary с передачей зоны (AXFR) на другом надёжнее рассылки каждой правки из хаба. Хаб помогает это настроить и показывает расхождения, но не дублирует записи сам. 8. **Перед изменением предпросмотр, после него итог и откат.** Каждое применение конфига сохраняет прежнюю версию. --- ## 3. Этапы Сначала безопасность, потом Apps (главный упор), потом общее управление. ### Этап 0. Безопасность записи (обязателен первым) - **Пользователи хаба с ролями**, в файле `/data/users.json`, пароли в виде хешей: - `viewer`: только смотрит; - `apps-admin`: управляет приложениями; - `operator`: блокировка и кеш; - `admin`: всё. - **Защита входа:** - ограничение попыток входа и временная блокировка после серии неудач; - по желанию: вход через ваш auth-портал (Traefik `forwardAuth`). - **Журнал действий** (`/data/audit.log`, раздел Audit в консоли хаба): кто, когда, на каком сервере, что сделал, итог. - **Токены записи** `MDNS_ADMIN_TOKEN` и `NDNS_ADMIN_TOKEN`. На старте права только Modify и Delete на Apps, другие разделы добавляются по этапам. - **Права в хабе:** вместо списка «только чтение»: таблица «эндпоинт → чтение / запись на один сервер / запись на все / нужная роль». - **Доступ к хабу по сети:** закрыть по IP (`ipallowlist` в Traefik) или через auth-портал. ### Этап 1. Apps: обзор по серверам (только чтение) Матрица «приложение × сервер» в разделе Apps при «All servers»: - установлено или нет, версия, есть ли обновление; - конфиг: «совпадает», «отличается (N полей)» или «отличается только в локальных полях»; - фильтры: «отличаются версии», «отличаются конфиги», «есть обновление». Отвечает на вопрос «что где стоит и где расхождения» без единой записи. ### Этап 2. Apps: установка, обновление, удаление на нескольких серверах - Выбор цели у действий: mdns, ndns или все серверы. - **Из магазина:** `apps/downloadAndInstall` и `apps/downloadAndUpdate` на каждом выбранном сервере. - **Свой zip:** загружается в хаб один раз, хаб отправляет его через `apps/install` или `apps/update` на каждый сервер. - Окно итога по серверам и кнопка «Повторить на неудавшихся». - **«Выровнять версии»:** обновить приложение везде до самой новой версии. - **Удаление на всех:** отдельное подтверждение. Хаб перед удалением сохраняет конфиг в историю. ### Этап 3. Apps: сравнение и синхронизация конфигов - **Сравнение рядом:** конфиг на mdns и на ndns. Для JSON: по полям (добавлено, удалено, изменено), для не-JSON: построчно. - **Эталон в хабе:** - импорт эталона с выбранного сервера; - правка эталона в редакторе хаба; - хранение в `/data/apps/<приложение>/baseline.json`. - **Локальные поля:** - список путей полей, которые не синхронизируются (например, `networks`, `dbPath`), плюс их значения по каждому серверу; - хранение в `/data/apps/<приложение>/overrides.json`; - итоговый конфиг сервера = эталон + его локальные значения. - **Предпросмотр:** что изменится на каждом сервере, затем применение через `apps/config/set` на выбранных серверах с итогом по каждому. - **Проверка до записи:** JSON разбирается в хабе. Если сервер отверг конфиг, показывается его ошибка, а сервер остаётся со старым конфигом. ### Этап 4. Apps: история, откат, контроль расхождений - **История:** каждое применение сохраняет прежний конфиг каждого сервера (`/data/apps/<приложение>/history/`). Можно посмотреть любую версию и откатить один сервер или все. - **Проверка расхождений по расписанию** (по умолчанию раз в 15 минут). Если конфиг поменяли прямо в консоли сервера, в шапке появляется предупреждение с действиями «Принять как эталон» и «Вернуть к эталону». - **Экспорт и импорт всех эталонов одним файлом:** резервная копия и подготовка нового сервера. ### Этап 5. Управление каждым сервером из одной консоли - Запись на выбранный сервер открывается для разделов Zones, Cache, Blocking, DNS Client, Settings, DHCP, Logs (кроме удаления логов). Права токена `hub-admin` расширяются под эти разделы. - В «All servers» запись по-прежнему закрыта: кнопки неактивны с подсказкой «выберите сервер». - Администрирование пользователей, SSO и LDAP серверов остаётся в их собственных консолях. ### Этап 6. Действия «на все серверы» вне Apps - **Блокировка:** добавить или удалить домен в Blocked и Allowed, временно отключить блокировку, обновить списки, изменить URL списков. - **Кеш:** сбросить весь кеш или удалить домен. - **Итог по каждому серверу** и повтор на неудавшихся. ### Этап 7. Сравнение и выравнивание остального - **Zones в «All servers»:** - объединённый список с отметкой, где зона есть, и признаком расхождения (разный SOA serial); - действия «Скопировать зону на другой сервер» (экспорт и импорт) и «Сделать secondary на другом сервере» (передача зоны). - **Blocking и Settings:** отчёт о различиях и «Принять как эталон» с предпросмотром, по той же схеме «общая часть плюс локальные поля», что и для Apps. ### Этап 8. Удобство (по желанию) - Профили: набор приложений с конфигами и правил, который применяется к новому серверу. - Уведомления о расхождениях наружу (webhook). --- ## 4. Что меняется в коде **Хаб (`hub/`):** - `server.ts`: - таблица прав по эндпоинтам; - пересылка POST и multipart (zip приложений, импорт зон); - рассылка «на все» с итогами по серверам. - Новые модули: - `users.ts`: роли, хеши паролей, ограничение попыток входа; - `audit.ts`: журнал действий; - `store.ts`: файлы в `/data` (эталоны, локальные поля, история), запись атомарно через временный файл; - `apps-sync.ts`: сравнение конфигов, слияние эталона с локальными полями, предпросмотр, применение; - `drift.ts`: проверка расхождений по расписанию. - `config.ts`: `_ADMIN_TOKEN`, `HUB_DATA` (по умолчанию `/data`), `HUB_DRIFT_MINUTES`. **Консоль (`src/`), только в режиме хаба:** - права разделов приходят от хаба по роли пользователя; - Apps: матрица по серверам, выбор цели у действий, окно итогов; - экран «Конфиг приложения»: сравнение рядом, эталон, локальные поля, предпросмотр, история и откат; - предупреждение о расхождениях в шапке; - раздел Audit. **Развёртывание:** - в compose: том `technitium-hub-data:/data` и токены `MDNS_ADMIN_TOKEN`, `NDNS_ADMIN_TOKEN` в `.env`; - на каждом сервере пользователь `hub-admin` в отдельной группе с правами записи только на нужные разделы. --- ## 5. Риски и как их закрыть | Риск | Что делаем | |---|---| | Хаб получает ключи на запись к обоим серверам | Этап 0 первым; токены только на нужные разделы; хаб закрыт по IP или через auth-портал | | Частичный успех при рассылке | Итог по каждому серверу, повтор на неудавшихся, откат из истории | | Разные версии приложения ждут разный формат конфига | Хаб показывает версии и предлагает сначала выровнять их | | Неверно заданные локальные поля перенесут настройки одного сервера на другой | Предпросмотр перед каждым применением, история и откат | | Конфиг поменяли мимо хаба | Проверка расхождений по расписанию и предупреждение | | Разные версии Technitium понимают API по-разному | Версия каждого сервера в хабе, предупреждение при расхождении | --- ## 6. Как проверяется каждый этап - **Модульные тесты хаба** (vitest): права по эндпоинтам, роли и защита входа, слияние эталона с локальными полями, сравнение JSON, история и откат. - **Тест с двумя фейковыми серверами:** рассылка, частичный отказ, итоги. - **Мок** (`dev/mock-api.mjs`) получает приложения и конфиги, разные на двух портах. В браузере проверяются матрица, сравнение, применение и откат. - **Порядок работы:** код по этапам пишет Sonnet, Opus проверяет дифф, тесты и браузер, коммит после каждого принятого этапа. - **На серверах:** сначала на одном приложении с безобидным конфигом, потом шире. --- ## 7. Решения перед стартом 1. **Источник правды для конфигов:** эталон в хабе (рекомендация) или ведущий сервер mdns. 2. **Вход в хаб:** свои пользователи хаба с ролями или ваш auth-портал через Traefik `forwardAuth`. 3. **Какие приложения стоят на серверах** и какие поля в их конфигах заведомо локальные. Это станет начальными списками локальных полей.