# Хаб: запуск по шагам Хаб показывает статистику двух серверов Technitium DNS (`mdns.shnt.top` и `ndns.shnt.top`) в одной консоли. Это один маленький контейнер. Он отдаёт ту же консоль и сам ходит в API обоих серверов по их токенам: - запрос «All servers» хаб отправляет на оба сервера и складывает ответы; - запрос к одному серверу хаб пересылает только ему; - менять хаб умеет только приложения (установка, обновление, удаление), и только для пользователей с ролью `apps-admin` или `admin`. Остальные запросы на изменение получают отказ и попадают в журнал. Серверы Technitium не меняются, кластер не нужен. В инструкции хаб ставится на хост, где уже работают Traefik и `technitium-smart` (`docs/compose.shnt.example.yaml`). Адрес хаба: `https://dnss.shnt.top`. ``` браузер ─► Traefik (dnss.shnt.top, TLS) ─► technitium-hub:8080 ├─► https://mdns.shnt.top/api/... (MDNS_TOKEN) └─► https://ndns.shnt.top/api/... (NDNS_TOKEN) ``` Что хаб показывает: Dashboard (сумма или один сервер), Logs (View Logs и Query Logs, объединённые с колонкой Server), Apps, About и разделы управления одним сервером: Zones, Cache, Blocking (Blocked и Allowed), DNS Client, Settings и DHCP. В этих разделах хаб начинает с первого сервера, а менять что-либо можно только на выбранном сервере и только ролью, у которой есть нужное право (таблица в шаге 1а). Остаётся в консоли каждого сервера: Administration (пользователи, группы, сессии, токены, SSO, LDAP), удаление логов и статистики, резервная копия и восстановление настроек (в копии лежат пользователи и токены), права на зоны и настройки веб-службы сервера (адреса, порты, HTTPS), через которую хаб сам ходит на сервер. --- ## Шаг 0. Образ хаба в реестре Образ `git.shx.one/denozord/technitium-hub` собирает отдельный workflow `.gitea/workflows/hub.yaml`. 1. Отправьте коммиты хаба на `git.shx.one`: ```bash git push origin main ``` 2. В репозитории на `git.shx.one` откройте **Actions** и дождитесь зелёного прогона **Hub**: - job `check` проверяет типы и тесты хаба; - job `image` собирает образ и кладёт его в реестр. 3. Убедитесь, что образ появился: в репозитории или профиле откройте **Packages**, там должен быть `technitium-hub` с тегом `latest`. Если job `image` упал на входе в реестр, проверьте секрет `ACTION_PAT` (или `ACTIONS_PAT`) в настройках репозитория: это токен Gitea с правом `write:package`. Он тот же, что использует образ консоли. Выпуск версии с номером (необязательно): ```bash git tag hub-v1.0.0 && git push origin hub-v1.0.0 ``` Тег даёт образы `1.0.0` и `1.0` в дополнение к `latest`. --- ## Шаг 1. Пользователь только для чтения на каждом сервере Сделайте это **на обоих серверах**: `https://mdns.shnt.top` и `https://ndns.shnt.top`. Войдите в консоль сервера как администратор. 1. **Группа.** Administration › Groups › **Add Group**: имя `hub-readers`, описание любое. 2. **Права группы.** Administration › Permissions: у группы `hub-readers` отметьте только **View** в каждом разделе, который хаб показывает: | Раздел | Зачем хабу | |---|---| | Dashboard | статистика, топы, график по часам, живые метрики | | Logs | список файлов логов и Query Logs | | Apps | список приложений логирования, из которых Query Logs читает записи | | Settings | настройки и состояние блокировки | | Zones | список зон, записи, экспорт зоны | | Cache | дерево кеша | | Allowed | список разрешённых доменов и его экспорт | | Blocked | список заблокированных доменов и его экспорт | | DnsClient | DNS-запрос из раздела DNS Client | | DhcpServer | области и аренды DHCP | Без View на новом разделе этот раздел в хабе отвечает «в доступе отказано». Секретные значения настроек (ключи TSIG, пароли) хаб скрывает от всех ролей, кроме `admin`. Modify и Delete не отмечайте нигде. Остальные разделы оставьте без прав. 3. **Пользователь.** Administration › Users › **Add User**: имя `hub-reader`, длинный случайный пароль. Затем в карточке пользователя добавьте его в группу `hub-readers`. Из группы `Everyone` можно убрать, если у неё есть лишние права. 4. **API-токен.** Нужен токен именно для `hub-reader`, а не для администратора: - выйдите из консоли и войдите под `hub-reader`; - в меню пользователя внизу слева выберите **Create API Token**, имя токена `hub`; - скопируйте токен сразу: его показывают один раз. Можно и под администратором: Administration › Sessions › **Create Token**, пользователь `hub-reader`. 5. Проверьте токен с любой машины. В ответе должно быть `"status":"ok"`: ```bash curl -s "https://mdns.shnt.top/api/dashboard/stats/get?type=LastHour&token=ТОКЕН" | head -c 200 ``` - `"status":"invalid-token"` значит, что скопирован не тот токен; - `"status":"error"` с сообщением об отказе в доступе значит, что у группы не хватает права View. Результат шага: два токена, `MDNS_TOKEN` (с `mdns.shnt.top`) и `NDNS_TOKEN` (с `ndns.shnt.top`). ### Шаг 1а. Пользователь для записи (нужен, чтобы менять приложения) Без этого шага хаб только читает. Токены записи отдельные, у отдельного пользователя. Сделайте это **на обоих серверах**: 1. **Группа** `hub-admins`. Права группы (Administration › Permissions) дают только то, что хаб может сделать этим токеном: | Раздел | Права | Что хаб пишет | |---|---|---| | Apps | View, Modify, Delete | установка, обновление, удаление, конфиги приложений | | Zones | View, Modify, Delete | зоны, записи, DNSSEC, импорт | | Cache | View, Delete | сброс кеша и удаление домена из кеша | | Allowed, Blocked | View, Modify, Delete | добавить, удалить, очистить, импортировать | | Settings | View, Modify | сохранение настроек и URL списков блокировки, временное отключение блокировки, включение блокировки, обновление списков. Delete не нужен: резервная копия и восстановление через хаб закрыты | | DnsClient | View, Modify | Import из DNS Client пишет ответ в зону | | DhcpServer | View, Modify, Delete | области и аренды | | Dashboard, Logs | View | чтобы этот токен мог читать то же, что `hub-reader`. Delete не нужен: удаление логов и статистики закрыто | | Administration | без прав | хаб это не использует | Выдавайте Modify и Delete только в тех разделах, которыми собираетесь управлять из хаба; без права на сервере запись просто будет отклонена. 2. **Пользователь** `hub-admin` в этой группе, длинный случайный пароль. 3. **API-токен** для `hub-admin` (так же, как в шаге 1.4). Это `MDNS_ADMIN_TOKEN` и `NDNS_ADMIN_TOKEN`. Хаб использует эти токены только для записи (приложения, зоны, кеш, блокировка, DNS Client с импортом, настройки, DHCP), для чтения конфига приложения перед изменением и для сверки веб-службы перед сохранением настроек. Если для сервера токена нет, хаб показывает его «только для чтения» и не вызывает для записи: кнопки неактивны, а под выбором сервера написано «No admin token configured on the hub». Запись идёт на один выбранный сервер. В режиме «All servers» (Dashboard, Logs, Blocking › Overview) кнопки записи неактивны с подсказкой «Choose a server». Действия на несколько серверов сразу (этап 6): в «All servers» кнопки Block, Allow, Disable в Blocking › Overview, «Update lists on all servers», «Edit list URLs on all servers», «Remove from Blocked» и «Remove from Allowed», а также «Flush on all servers» и «Delete a domain on all servers» в Cache и Block/Allow в строках Query Logs открывают выбор серверов. По умолчанию отмечены все серверы с токеном записи; сервер без токена виден, но отключён, с причиной. Итог показывается по каждому серверу, «Retry failed» повторяет только неудавшиеся. Для этого нужны те же права на серверах: Blocked и Allowed (Modify, Delete), Cache (Delete), Settings (Modify: URL списков и переключатель блокировки). Роли `operator` и `admin`; в аудит попадает по строке на сервер, список URL в аудит не пишется. Заменённый список URL полностью перезаписывает прежний на каждом выбранном сервере. Зоны, настройки и блокировка на нескольких серверах (этап 7), только роль `admin`. Zones при «All servers» показывает общую таблицу: где зона есть, её тип и SOA serial, статус (одинаково, serial отличается, тип отличается, только на одном сервере; Secondary с отставшим serial отмечен, но расхождением не считается). В меню строки: «Open on …», «Copy to …» (экспорт зоны с одного сервера и импорт на другой; копируются только Primary и Forwarder, подписанные DNSSEC и Secondary не копируются; если зона уже есть, нужна галочка Overwrite) и «Make secondary on …» (хаб добавляет адрес второго сервера в передачу зоны и уведомления первого и создаёт Secondary на втором; адрес первого, как его видит второй, подставляется из адреса сервера в хабе, адрес второго для списка передачи вводится вручную; по желанию ключ TSIG, он должен быть на обоих серверах). Для этого на **обоих** серверах у `hub-admin` нужны Zones (View, Modify, Delete) и Settings (View, Modify). Кнопка «Compare servers» в Settings и в Blocking открывает сравнение настроек (вкладки Compare, Baseline, Local fields, History): эталон берётся с выбранного сервера кнопкой «Accept … as baseline», применение (Apply) отправляет только изменившиеся общие поля и перед записью сохраняет прежние настройки сервера в историю. По умолчанию **локальны** и не отправляются: `dnsServerDomain`, адреса привязки (`dnsServerLocalEndPoints`, `dnsServerIPv4SourceAddresses`, `dnsServerIPv6SourceAddresses`), все `webService*`, сертификат DNS (`dnsTlsCertificatePath`, `dnsTlsCertificatePassword`), сокеты и адреса обратного прокси этого хоста, `logFolder`; версия, время работы, данные кластера и временное отключение блокировки не сравниваются. Секреты (ключи TSIG, пароль прокси) сравниваются внутри хаба, нигде не показываются (только «secret changed») и в аудит не пишутся; файлы эталона и истории хранят их в `/data` с правами 0600. В Blocking сравниваются ещё свои списки Blocked и Allowed как множества («не хватает N, лишних M»); лишние удаляются только при галочке «Also remove extras». Какие роли хаба что могут: `operator` меняет кеш, списки Blocked и Allowed, переключатель блокировки (включить, временно отключить) и обновляет списки, `admin` меняет и зоны, настройки и DHCP; `viewer` и `apps-admin` в этих разделах только читают. Хаб отклоняет всё, чего нет в таблице путей: `admin/*` и `user/*` (кроме собственного входа хаба), `logs/delete`, `logs/deleteAll`, `dashboard/stats/deleteAll`, `settings/backup`, `settings/restore`, права на зоны (`zones/permissions/*`). Сохранение настроек с изменённым полем веб-службы сервера (`webService*`) или с полем пользователей, SSO и LDAP хаб тоже отклоняет с сообщением «Change the web service settings in that server's own console.» --- ## Шаг 2. DNS-запись для dnss.shnt.top В Cloudflare (зона `shnt.top`) создайте запись `dnss` типа `A` на IP хоста, где работает хаб. Сертификат Traefik получит сам. В compose уже настроен `certresolver=myresolver` с DNS-challenge через Cloudflare (`CF_DNS_API_TOKEN`), отдельно открывать порт 80 для проверки не нужно. --- ## Шаг 3. Файл .env На хосте в каталоге с `docker-compose.yml` откройте `.env` (там уже лежат `CF_DNS_API_TOKEN` и `DNS_SERVER_ADMIN_PASSWORD`) и добавьте: ```dotenv # Хаб: токены пользователей hub-reader с шага 1 MDNS_TOKEN=вставьте_токен_mdns NDNS_TOKEN=вставьте_токен_ndns # Токены hub-admin с шага 1а (без них хаб не меняет приложения) MDNS_ADMIN_TOKEN=вставьте_токен_записи_mdns NDNS_ADMIN_TOKEN=вставьте_токен_записи_ndns # Первый вход в хаб (это НЕ логин Technitium). Нужен, пока не создан файл пользователей (шаг 4a) HUB_USER=admin HUB_PASSWORD=длинный_случайный_пароль ``` Пароль лучше длинный. После 5 неудачных попыток за 15 минут вход блокируется на 15 минут, но это не замена хорошему паролю. Сгенерировать можно так: ```bash openssl rand -base64 24 ``` Закройте `.env` от посторонних: ```bash chmod 600 .env ``` --- ## Шаг 4. Сервис в compose Добавьте в `docker-compose.yml` в раздел `services:` блок из `docs/compose.hub.example.yaml`: ```yaml technitium-hub: image: git.shx.one/denozord/technitium-hub:latest container_name: technitium-hub restart: unless-stopped networks: - dns-net environment: # id=адрес, по порядку; токен каждого берётся из _TOKEN - HUB_SERVERS=mdns=https://mdns.shnt.top,ndns=https://ndns.shnt.top - MDNS_TOKEN=${MDNS_TOKEN} - NDNS_TOKEN=${NDNS_TOKEN} - MDNS_ADMIN_TOKEN=${MDNS_ADMIN_TOKEN} - NDNS_ADMIN_TOKEN=${NDNS_ADMIN_TOKEN} - HUB_USER=${HUB_USER} - HUB_PASSWORD=${HUB_PASSWORD} - HUB_DATA=/data - HUB_TRUST_PROXY=1 - HUB_DOMAIN=dnss.shnt.top - TZ=Asia/Novosibirsk volumes: - technitium-hub-data:/data labels: - "traefik.enable=true" - "traefik.docker.network=dns-net" - "traefik.http.routers.hub.rule=Host(`dnss.shnt.top`)" - "traefik.http.routers.hub.entrypoints=websecure" - "traefik.http.routers.hub.tls.certresolver=myresolver" - "traefik.http.routers.hub.service=hub-svc" - "traefik.http.services.hub-svc.loadbalancer.server.port=8080" ``` Том `technitium-hub-data` нужно объявить в разделе `volumes:` того же файла: ```yaml volumes: technitium-hub-data: ``` В нём лежат файл пользователей `users.json` и журнал `audit.log`. `HUB_TRUST_PROXY=1` нужен за Traefik: хаб берёт адрес клиента из заголовка `X-Forwarded-For` для ограничения попыток входа и журнала. Если до хаба можно достучаться в обход Traefik, включать эту переменную нельзя: заголовок можно подделать. Если хаб стоит в том же файле, что Traefik, отдельный блок `networks:` с `external: true` из примера не нужен: сеть `dns-net` в этом файле уже объявлена. Про имена в `HUB_SERVERS`: - имя слева от `=` видно в консоли в селекторе «Server»; - из имени получается переменная токена: `mdns` даёт `MDNS_TOKEN`, `ndns` даёт `NDNS_TOKEN`, `my-dns` дал бы `MY_DNS_TOKEN`; - первый в списке сервер считается основным. Адреса серверов указываются публичные (`https://mdns.shnt.top`). API Technitium доступно по тому же адресу, что и консоль (роутер `panel-old` в Traefik), отдельный порт не нужен. --- ## Шаг 4a. Пользователи хаба У хаба свои пользователи с ролями. Они лежат в `/data/users.json`, пароли в виде хешей. | Роль | Что может | |---|---| | `viewer` | смотреть | | `operator` | смотреть, блокировка и кеш (после появления записи) | | `apps-admin` | смотреть, устанавливать, обновлять и удалять приложения на выбранных серверах | | `admin` | всё, включая журнал действий | Пока файла нет, в хаб входит один `admin` из `HUB_USER` / `HUB_PASSWORD`. Когда файл появился, эти две переменные игнорируются (в логе будет строка об этом). Если нет ни файла, ни переменных, хаб не запустится. После запуска (шаг 5) создайте пользователей. Пароль команда спросит дважды, при вводе он не показывается: ```bash docker exec -it technitium-hub node --experimental-strip-types /app/hub/users-cli.ts add denis admin docker exec -it technitium-hub node --experimental-strip-types /app/hub/users-cli.ts add alice viewer ``` Остальные команды: `passwd <имя>`, `role <имя> <роль>`, `disable <имя>`, `enable <имя>`, `list`. Хаб замечает изменение файла на следующем запросе и завершает сессии затронутых пользователей. Перезапуск не нужен. После создания своего `admin` уберите `HUB_USER` и `HUB_PASSWORD` из `.env` и compose, затем `docker compose up -d technitium-hub`. Журнал: войдите админом и откройте `/api/hub/audit/list?limit=200` (JSON, новые записи первыми) или смотрите файл `docker exec technitium-hub tail -n 50 /data/audit.log`. Пароли и токены в журнал не пишутся. --- ## Шаг 5. Запуск Если реестр закрытый, один раз войдите в него. Нужен токен Gitea с правом `read:package`: ```bash docker login git.shx.one ``` Скачайте образ и запустите хаб: ```bash docker compose pull technitium-hub docker compose up -d technitium-hub ``` Проверьте контейнер: ```bash docker compose ps technitium-hub # STATUS: Up ... (healthy) docker compose logs -f technitium-hub ``` В логе при старте не должно быть ошибок конфигурации. Если чего-то не хватает, хаб не запустится и назовёт переменную, например `NDNS_TOKEN is required for the server "ndns".` --- ## Шаг 6. Проверка 1. Откройте `https://dnss.shnt.top` и войдите с `HUB_USER` / `HUB_PASSWORD` из `.env`. 2. На Dashboard над заголовком должен быть селектор **Server: All servers**. 3. Сверьте цифры. Total Queries в «All servers» должна равняться сумме значений в консолях `mdns.shnt.top` и `ndns.shnt.top` за тот же период. Расхождение в пределах минуты нормально: хаб кеширует ответы на 15 секунд, а Last Hour обновляется на ходу. 4. Выберите в селекторе `mdns`, затем `ndns`: цифры каждого сервера должны совпасть с его собственной консолью. 5. Откройте Logs › Query Logs, нажмите **Query**. Записи обоих серверов идут вместе, в колонке **Server** видно, чья запись. 6. Кнопка статуса справа в шапке показывает `dnss.shnt.top`. Если какой-то сервер не ответил, там будет «1 server not answering», а по нажатию появится причина. Проверка без браузера (подставьте пароль хаба): ```bash T=$(curl -s -X POST https://dnss.shnt.top/api/user/login \ --data-urlencode "user=admin" --data-urlencode "pass=ПАРОЛЬ_ХАБА" | sed -E 's/.*"token":"([^"]+)".*/\1/') curl -s -H "Authorization: Bearer $T" "https://dnss.shnt.top/api/dashboard/stats/get?type=LastHour" | head -c 300 curl -s -H "Authorization: Bearer $T" "https://dnss.shnt.top/api/dashboard/stats/get?type=LastHour&node=ndns" | head -c 300 ``` --- ## Обновление После нового прогона **Hub** в Actions: ```bash docker compose pull technitium-hub && docker compose up -d technitium-hub ``` Сессии хаба хранятся в памяти: после перезапуска контейнера нужно войти заново. Чтобы закрепить версию, укажите в compose тег вместо `latest`, например `technitium-hub:1.0.0`. ## Остановка и удаление ```bash docker compose stop technitium-hub # остановить docker compose rm -f technitium-hub # удалить контейнер ``` Чтобы отозвать доступ хаба к серверам, на каждом сервере удалите токен в Administration › Sessions или отключите пользователя `hub-reader`. --- ## Настройки хаба | Переменная | По умолчанию | Что задаёт | |---|---|---| | `HUB_SERVERS` | обязательна | Серверы по порядку: `id=https://адрес,...` | | `_TOKEN` | обязательна для каждого | API-токен сервера (`MDNS_TOKEN`, `NDNS_TOKEN`, ...) | | `HUB_USER`, `HUB_PASSWORD` | нужны, пока нет `users.json` | Первый `admin`; при наличии файла игнорируются | | `HUB_DATA` | `/data` | Папка `users.json` и `audit.log` (на ней том) | | `HUB_TRUST_PROXY` | выключено | `1`: адрес клиента из `X-Forwarded-For` (только за Traefik) | | `_ADMIN_TOKEN` | нет | Токен записи сервера (`MDNS_ADMIN_TOKEN`). Им хаб меняет приложения; без него сервер только для чтения. В логи не пишется | | `HUB_DOMAIN` | `hub` | Имя, которое консоль показывает как имя сервера | | `HUB_CACHE_SECONDS` | `15` | Сколько секунд хранить ответы серверов (0 выключает кеш) | | `HUB_TIMEOUT_MS` | `8000` | Сколько ждать ответа сервера, мс | | `HUB_PORT` | `8080` | Порт внутри контейнера | | `HUB_WEBHOOK_URL` | нет | Адрес http или https, куда хаб шлёт уведомления (см. ниже). Адрес может содержать токен, храните его как секрет | | `HUB_WEBHOOK_SECRET` | нет | Подписывает уведомления (`X-Hub-Signature`). В логи не пишется | Третий сервер добавляется строкой в `HUB_SERVERS` и его токеном: ```yaml - HUB_SERVERS=mdns=https://mdns.shnt.top,ndns=https://ndns.shnt.top,lab=https://lab.shnt.top - LAB_TOKEN=${LAB_TOKEN} ``` --- ## Если что-то не так | Признак | Причина и что делать | |---|---| | Контейнер сразу завершается | В `docker compose logs technitium-hub` будет названа недостающая переменная. Проверьте `.env` и что compose его видит (`docker compose config \| grep TOKEN`). | | `https://dnss.shnt.top` не открывается, ошибка сертификата | Нет DNS-записи `dnss` (шаг 2) или Traefik ещё получает сертификат: смотрите `docker logs traefik`. | | 404 от Traefik | Контейнер не в сети `dns-net` или опечатка в `Host(\`dnss.shnt.top\`)`. | | Вход в хаб: «Invalid username or password» | Неверный логин или пароль. Если есть `users.json`, работают только его пользователи (`users-cli.ts list`), а `HUB_USER` игнорируется. После правки `.env` нужен `docker compose up -d technitium-hub`. | | «Too many failed logins. Try again in N minutes.» | 5 неудач за 15 минут с одного адреса или на одно имя. Подождите; блокировка хранится в памяти, её снимает перезапуск контейнера. | | Все пользователи блокируются разом | Не включён `HUB_TRUST_PROXY=1`: хаб видит только адрес Traefik. | | Контейнер пишет `audit log is not writable` | Нет тома на `/data` или папка не принадлежит `node`. Журнал живёт в памяти (последние 1000 записей) до исправления. | | В шапке «1 server not answering», в подсказке `ndns: its API token was refused` | Токен `NDNS_TOKEN` отозван или скопирован с ошибкой. Создайте новый (шаг 1.4) и перезапустите хаб. | | В подсказке `... did not answer: ...` или `ECONNREFUSED` / `timeout` | Хаб не достучался до сервера. Проверьте с хоста хаба: `docker exec technitium-hub node -e "fetch('https://ndns.shnt.top/api/status').then(r=>console.log(r.status))"`. | | В подсказке `ndns: ...` с сообщением об отказе в доступе | У группы `hub-readers` на этом сервере не хватает View в каком-то разделе (шаг 1.2). | | Query Logs пустой или «нет приложения» | На сервере не установлено приложение логирования (Query Logs (Sqlite) и подобные) или нет права View на Apps. | | Цифры «All servers» больше суммы на 1-2 клиента | Clients считается по каждому серверу отдельно: клиент, который ходит на оба сервера, учтён дважды. Это ожидаемо. | | Нажатие на что-то пишет «The hub is read-only.» | Хаб меняет только приложения. Остальные изменения делаются в консоли нужного сервера. | ## Конфиги приложений: эталон и синхронизация Чтобы конфиги приложений на серверах были одинаковыми, кроме полей, которые у каждого сервера свои (путь к базе, сети): 1. Для чтения конфигов `hub-reader` нужно право **View на Apps** (оно уже есть в таблице шага 1). Если сервер откажет, хаб попробует токен `hub-admin`. 2. Откройте Apps › All servers, в меню строки приложения выберите **Config…** (или нажмите значок «Different · N»). Читать сравнение могут все роли. 3. **Эталон.** Вкладка Baseline: **Import from mdns** (или другого сервера, на котором конфиг правильный). Эталон можно поправить в редакторе и сохранить. Если текст начинается с `{` или `[`, он должен быть корректным JSON, иначе хаб покажет ошибку. 4. **Локальные поля.** Вкладка Local fields: добавьте путь поля, например `sqliteDbPath` или `networks.0.cidr`, нажмите **Fill from the current server configs** (подставит текущие значения каждого сервера) и сохраните. Такие поля не сравниваются и не рассылаются: в матрице серверы, различающиеся только ими, показываются как «Same». 5. **Применение.** Кнопка **Apply…**: выберите серверы, посмотрите предпросмотр (что изменится, предупреждения, например «версии отличаются, сначала выровняйте версии»), нажмите **Apply**. По каждому серверу будет итог; **Retry failed** повторит только неудавшиеся. Что хаб делает для безопасности: - перед записью сохраняет прежний конфиг сервера в `/data/apps/<имя>/history/` (50 последних) и не пишет, если сохранить не удалось; - если конфиг на сервере изменили после предпросмотра, этот сервер пропускается с пояснением; - после записи читает конфиг обратно и проверяет, что он совпал; если сервер отклонил конфиг, остаётся старый; - в журнал попадают кто, на каком сервере и размер с коротким хешем конфига, но не сам текст. Файлы эталона и локальных полей лежат в `/data/apps/<имя>/` (`baseline.json`, `overrides.json`). Они содержат конфиги, поэтому том `/data` нужно беречь так же, как `users.json`. ## История, откат, контроль расхождений, перенос эталонов - **История.** Вкладка History в окне Config: все прежние версии, которые хаб сохранил (конфиг сервера перед применением или сохранением, прежний эталон). Выберите версию, чтобы прочитать её и увидеть отличие от текущего конфига сервера. Роли `apps-admin` и `admin`: **Roll back…** (выбор серверов, предпросмотр, итог по каждому) и **Restore as baseline** для прежнего эталона. Хранится по 50 версий на приложение. - **Контроль расхождений.** Раз в 15 минут хаб читает конфиги и сравнивает их с тем, что должно быть (эталон плюс локальные поля сервера). Если конфиг поменяли прямо в консоли сервера, в шапке появляется «N config drift». В списке у каждого расхождения есть **Open config**, а для `apps-admin` и `admin` ещё **Accept as baseline** (конфиг этого сервера становится эталоном; локальные поля остаются локальными) и **Revert to baseline** (через предпросмотр). Период меняется переменной `HUB_DRIFT_MINUTES`, `0` отключает проверку. Сервер, который не ответил, показывается как «Cannot read» и в счётчик не входит. Последний результат лежит в `/data/drift.json`. - **Что проверяется.** Конфиги приложений, общие поля настроек (против эталона настроек) и списки Blocked/Allowed (против эталона списков). Проверка настроек и списков идёт только если соответствующий эталон принят. Расхождение настроек или списков показывает только названия полей, без значений. В списке у него **Open compare** (окно сравнения Settings или Blocking), **Accept as baseline** и **Revert to baseline** (через тот же предпросмотр); эти действия только для роли `admin`. - **Перенос эталонов.** Apps › All servers: **Export baselines** скачивает один файл со всеми эталонами и локальными полями, **Import baselines** загружает его в другой хаб: сначала проверка и сводка (New, Changed, Same), запись только после **Import**. На серверы импорт ничего не отправляет. Файл содержит конфиги, храните его как секрет. ## Уведомления (webhook) Если задан `HUB_WEBHOOK_URL`, хаб отправляет POST с JSON `{event, at, hub, items}`: - `drift.found`: что-то только что разошлось с эталоном (сервер, вид, имя). Шлётся один раз при изменении, а не каждые 15 минут; - `drift.cleared`: расхождение исчезло (сервер, который перестал отвечать, не считается исчезнувшим); - `write.failed`: изменение, записанное в журнал с исходом `failed` (без значений). Сбои одного момента идут одним уведомлением. Пример: ```json {"event":"drift.found","at":"2026-11-01T09:15:00.000Z","hub":"dnss.shnt.top", "items":[{"kind":"settings","server":"ndns","since":"2026-11-01T09:15:00.000Z","fields":["blockingType"]}]} ``` Если задан `HUB_WEBHOOK_SECRET`, заголовок `X-Hub-Signature: sha256=` содержит HMAC-SHA256 точных байт тела. Проверка на стороне получателя (Node): ```js const expected = 'sha256=' + createHmac('sha256', SECRET).update(rawBody).digest('hex') // сравните с заголовком через timingSafeEqual ``` Отправка не блокирует работу хаба: таймаут 5 секунд, не больше одного повтора. Ошибка пишется в лог хаба с именем хоста (без параметров адреса). Администратор видит состояние (настроен ли, хост, результат последней отправки) и кнопку **Send test** в окне «Config drift» (оно открывается из шапки, когда есть расхождения). ## Профили Профиль нужен, чтобы быстро привести новый или сброшенный сервер к общему виду. Это именованный набор в `/data/profiles/`: приложения магазина (имя и версия) с эталонным конфигом каждого, общие поля настроек (поля, относящиеся к самому хосту, не входят) и эталон списков Blocked/Allowed. Файл содержит секреты настроек (ключи TSIG), берегите том `/data`. Все действия только для роли `admin` (`apps-admin`, `operator` и `viewer` получают отказ). Кнопка **Profiles** на экране Apps › All servers: - **Create from current baselines**: профиль собирается из эталонов, которые хаб держит сейчас; можно не брать приложения, настройки или списки. Приложение не из магазина (поставленное из zip) в профиль не входит; - **View**: состав профиля; секреты показаны как ``, тексты конфигов не показываются; - **Apply to servers…**: выбор серверов, предпросмотр по шагам для каждого сервера, затем итог по каждому шагу. Шаги идут по порядку, ошибка шага останавливает следующие шаги этого сервера: 1) установка недостающих приложений из магазина самого сервера, 2) конфиги приложений (эталон плюс локальные поля сервера, прежний конфиг сохраняется в историю), 3) общие поля настроек (через те же проверки, что и обычное сохранение), 4) списки: недостающие домены добавляются, лишние удаляются только по галочке **Also remove extras**. **Retry failed** начинает заново с шага, который не удался; - **Export** и **Import profiles**: перенос между хабами (файл содержит секреты, храните его как секрет; сначала проверка, запись только после **Import**). ## Приложения: установка, обновление, удаление Роль `apps-admin` (или `admin`) видит в разделе Apps при «All servers» меню действий в каждой строке и кнопки **App Store** и **Install from file**: - **Update on all**: обновить с сервера, где есть обновление; - **Install on missing servers**: поставить из магазина там, где приложения нет; - **Align versions**: подтянуть отстающие серверы до самой новой установленной версии (если магазин её предлагает); - **Uninstall from…**: удалить с выбранных серверов. Каждое действие сначала открывает выбор серверов (сервер без токена записи недоступен, с причиной), потом показывает итог по каждому серверу. Если на каком-то сервере не вышло, кнопка **Retry failed** повторит только его. Что хаб делает для безопасности: - ссылку на скачивание берёт из магазина самого сервера, произвольный адрес не принимает; - перед удалением сохраняет конфиг приложения в `/data/apps/<имя>/history/` (50 последних на приложение) и не удаляет, если сохранить не удалось. Откат: вставить сохранённый текст в Config приложения на сервере; - каждое действие по каждому серверу пишется в журнал (`apps.update-store`, `apps.uninstall` и т. д.). Права на запись у `viewer` и `operator` нет: они получают «Your role does not allow this.». ## Безопасность - Читает хаб токенами с одними правами View. Менять он умеет только приложения, и только токенами `*_ADMIN_TOKEN` (права Apps) по запросу пользователя с ролью `apps-admin` или `admin`. Всё остальное получает отказ в хабе и запись в журнале. - Токены лежат только в `.env` на хосте и в памяти контейнера. В браузер они не попадают, в ответы и логи хаба не пишутся. - У каждого человека свой логин и роль. Вход защищён блокировкой после 5 неудач. Нужны длинные пароли, а если хаб не должен быть виден из интернета, добавьте к роутеру `hub` в Traefik `ipallowlist` или `basicAuth`.