The drift check now also covers the settings baseline and the Blocked and Allowed lists baseline; the drift dialog offers Open compare, Accept as baseline and Revert to baseline for them. Old drift.json files still load. HUB_WEBHOOK_URL (and an optional HUB_WEBHOOK_SECRET for an HMAC signature) receives drift.found, drift.cleared and write.failed notices, only on change, without values, never blocking a request and never logging the address beyond its host. Admins see the webhook status and can send a test from About and from the drift dialog. Profiles bundle the hub's app, settings and lists baselines. Applying one to a server runs install, app configs, settings and lists in order after a preview; a failed step stops that server's later steps and Retry failed resumes from it. Profiles export and import as a file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
424 lines
45 KiB
Markdown
424 lines
45 KiB
Markdown
# Хаб: запуск по шагам
|
||
|
||
Хаб показывает статистику двух серверов 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=адрес, по порядку; токен каждого берётся из <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://адрес,...` |
|
||
| `<ID>_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) |
|
||
| `<ID>_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=<hex>` содержит 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**: состав профиля; секреты показаны как `<secret>`, тексты конфигов не показываются;
|
||
- **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`.
|