API хаба и токены
Всё, что делает веб-интерфейс хаба, делает его JSON API. Для автоматизации — n8n, CI, свои скрипты — есть API-токены: доступ без пароля и сессии браузера, с ролью, пределами по хостам и группам, списком адресов и сроком.
Токены

«О системе» → «API-токены» → «Новый токен» (только администратор):
- Роль. Чтение — список хостов, состояние, проблемы, уязвимости, службы и контейнеры, оповещения, конвейеры и журналы заданий. Администратор — ещё и действия: выкладка и откат конвейера, сухой прогон, бан и разбан IP на хостах, действия со службами и контейнерами, сканирование уязвимостей.
- Хосты и группы — пределы токена. Пусто в обоих полях — все хосты. Хост в пределах, если он назван сам или входит в одну из групп (машина — в группе своего хоста). Переименование группы токен не ломает.
- Адреса — с каких адресов или подсетей токен принимается (пусто — с любых). Заголовкам прокси (
X-Forwarded-For) хаб верит, только когда соединение пришло с loopback — от своего обратного прокси; напрямую подделать адрес заголовком нельзя. - Срок — 30, 90, 365 дней или бессрочно.
- Через nkt-edge — токен принимается и через edge с ролью API (ниже).
Секрет показывается один раз, сразу после создания. Потерян — «Новый секрет»: старый перестаёт действовать сразу. «Отозвать» удаляет токен. Правка (имя, роль, пределы, адреса, срок) — в окне с диффом «что изменится». Создание, правка, новый секрет и отзыв пишутся в журнал действий; вызовы токена — от имени token:<имя>. В списке видно, когда и с какого адреса токен использовали в последний раз.
Токену недоступны ни с какой ролью: управление хабом (учётные записи, сами токены, экспорт и импорт, обновление хаба, edge, настройки ИИ, добавление и удаление хостов), терминал и консоль, файлы и бэкапы, содержимое конфигов, Kubernetes, веб-сокеты. Такой вызов — ответ 403.
Как предъявить токен
Bearer — просто; секрет идёт в каждом запросе, годится в своей сети или по HTTPS:
curl -H "Authorization: Bearer nkt_<ключ>_<секрет>" https://hub.example.com/api/hub/hostsПодписанный запрос — секрет по сети не передаётся, подпись живёт 5 минут и не повторяется. Так токен можно пускать и через промежуточный узел. Заголовки:
| Заголовок | Значение |
|---|---|
X-NKT-API-Key | ключ токена (16 символов) |
X-NKT-API-Timestamp | время, unix-секунды; расхождение с часами хаба — до 5 минут |
X-NKT-API-Nonce | случайная строка 16–128 символов A-Za-z0-9_-, новая на каждый запрос |
X-NKT-API-Signature | hex HMAC-SHA256 секретом от строки ниже |
Подписываемая строка — шесть строк через \n:
NKT-API-1
<timestamp>
<nonce>
<МЕТОД>
<путь?запрос> например /api/hub/events?after=120
<hex sha256 тела> пустое тело — sha256 пустой строкиBash:
KEY=abcdefghijklmnop SECRET='...'
TS=$(date +%s) NONCE=$(openssl rand -hex 16)
BODY='{"action":"ban","ips":["198.51.100.7"]}'
URI=/api/hub/fail2ban/fleet
HASH=$(printf %s "$BODY" | sha256sum | cut -d' ' -f1)
SIG=$(printf 'NKT-API-1\n%s\n%s\nPOST\n%s\n%s' "$TS" "$NONCE" "$URI" "$HASH" \
| openssl dgst -sha256 -hmac "$SECRET" | sed 's/.* //')
curl -X POST -H 'Content-Type: application/json' \
-H "X-NKT-API-Key: $KEY" -H "X-NKT-API-Timestamp: $TS" \
-H "X-NKT-API-Nonce: $NONCE" -H "X-NKT-API-Signature: $SIG" \
-d "$BODY" "https://hub.example.com$URI"JavaScript (Node, узел Code в n8n):
const crypto = require('crypto')
function sign(secret, method, uri, body = '') {
const ts = String(Math.floor(Date.now() / 1000))
const nonce = crypto.randomBytes(16).toString('hex')
const hash = crypto.createHash('sha256').update(body).digest('hex')
const sig = crypto.createHmac('sha256', secret)
.update(['NKT-API-1', ts, nonce, method.toUpperCase(), uri, hash].join('\n')).digest('hex')
return { 'X-NKT-API-Timestamp': ts, 'X-NKT-API-Nonce': nonce, 'X-NKT-API-Signature': sig }
}Тело подписанного запроса — до 1 МБ. Подпись считается от тела ровно в том виде, в каком оно уходит.
Снаружи через nkt-edge
Хаб за NAT — до API снаружи (n8n в облаке, CI) можно дойти через nkt-edge с ролью API: запрос идёт на https://api.example.com/api/…, edge передаёт его хабу по туннелю. Условия:
- у токена стоит галочка «через nkt-edge»;
- только подписанные запросы — Bearer и cookie edge не пропускает, секрет токена на VPS не попадает;
- список адресов токена сверяется с адресом клиента, который передал edge;
- путь подписывается тот же, что виден хабу:
/api/hub/hosts, без имени edge.
Что доступно токену
| Вызов | Роль | Что |
|---|---|---|
GET /api/auth/me | чтение | кто я: имя, роль и пределы токена |
GET /api/hub/hosts | чтение | хосты в пределах токена: статус, версия, проблемы |
GET /api/hub/events?after=N | чтение | оповещения новее номера N (опрос) |
GET /api/hub/fail2ban/banned | чтение | забаненные адреса по хостам |
GET /api/hub/sites | чтение | сайты |
GET /api/hub/pipelines, /{id}, /{id}/deployments | чтение | конвейеры и история выкладок |
GET /api/hub/jobs/{id}, /{id}/log?after=N | чтение | задание хаба и его журнал |
GET /api/hosts/{id}/<раздел> | чтение | overview, findings, vulnerabilities, services, containers, certificates, monitor, jobs, fail2ban, firewall, updates и другие разделы хоста |
POST /api/hub/pipelines/{id}/deploy | администратор | выкладка ({"ref":"","tag":""}) → job_id |
POST /api/hub/pipelines/{id}/rollback | администратор | откат на выкладку |
POST /api/hub/pipelines/dryrun | администратор | сухой прогон ({"pipeline_id":1,"skip":[]}) → job_id |
POST /api/hub/fail2ban/fleet | администратор | бан или разбан ({"action":"ban","ips":[…],"host_ids":[…]}) |
POST /api/hosts/{id}/vulnerabilities/scan | администратор | сканирование уязвимостей |
POST/PUT/DELETE /api/hosts/{id}/<раздел>/… | администратор | действия хоста: службы, контейнеры, обновления пакетов и прочее, кроме закрытого выше и загрузки файлов (архивы образов, образы машин, файлы проводника, бэкапы — у них свои маршруты, токену они закрыты) |
POST /api/hosts/{id}/images/archives/fetch | администратор | скачать архив образа по ссылке на хост и загрузить в Docker или Podman → job_id (см. ниже) |
Машина хаба — /api/hosts/local/…, в пределах токена её номер -1. Токен с пределами видит только свои хосты, их оповещения и сайты; конвейеры — только compose-стеки, все хосты которых в его пределах, и сухой прогон — только сохранённого конвейера. Задания хаба токен с пределами видит только запущенные им самим.
Выкладка и сухой прогон — задания: ответ — job_id, итог — в GET /api/hub/jobs/{id} (status: queued, running, succeeded, failed), журнал — в /log?after=<номер последней строки>.
Загрузка образов Docker и Podman
Собранный у себя образ на хосты — два пути.
С компьютера — по входу администратора (токену загрузка файлов закрыта). Архив идёт на хост через хаб по отдельному SSH-соединению; до 10 ГБ, срок передачи через хаб — 6 часов. load=1 — как только файл получен целиком, хост сам запускает docker load (или podman load) заданием:
HUB=https://hub.example.com HOST=3 # номер хоста: GET /api/hub/hosts; машина хаба — local
curl -s -c jar -H 'Content-Type: application/json' \
-d '{"username":"admin","password":"…"}' $HUB/api/auth/login
docker save app:1.2 | gzip > app.tar.gz
curl -s -b jar -T app.tar.gz \
"$HUB/api/hosts/$HOST/images/archives/upload?name=docker__app-1.2.tar.gz&load=1"
# {"name":"docker__app-1.2.tar.gz","size":…,"job_id":42}
# &remove=1 — после успешного docker load удалить архив с хоста
curl -s -b jar $HUB/api/hosts/$HOST/jobs/42 # status: queued, running, succeeded, failed
curl -s -b jar "$HUB/api/hosts/$HOST/jobs/42/log?after=0"Имя архива — docker__<имя> или podman__<имя> (в какой движок загружать) с расширением .tar, .tar.gz, .tgz, .tar.xz или .tar.zst; архив с тем же именем заменяется. Без load=1 архив только ложится в «Архивы образов» хоста. По нескольким хостам — тот же curl -T в цикле по номерам.
По ссылке — с токеном. Хост сам скачивает архив (свой HTTP-сервер, хранилище артефактов CI, S3 по подписанной ссылке) и загружает его в движок; ссылка должна открываться с хоста:
curl -s -H "Authorization: Bearer nkt_<ключ>_<секрет>" -H 'Content-Type: application/json' \
-d '{"engine":"docker","url":"https://files.example.com/app-1.2.tar.gz","checksum":"<sha256, по желанию>","load":true}' \
$HUB/api/hosts/3/images/archives/fetch
# {"job_id":43} — ход и итог: GET /api/hosts/3/jobs/43"remove": true — удалить архив после успешной загрузки; name — по желанию, иначе — из ссылки; checksum — sha256 архива, неверная сумма — задание завершается ошибкой, и архив не остаётся. Токену нужна роль «администратор», хост — в его пределах.
Ответы об ошибках
| Код | Когда |
|---|---|
| 401 | неверный токен или подпись, подпись устарела или уже использована, срок истёк |
| 403 | адрес не в списке, вызов закрыт токену, роль «чтение» для действия, хост вне пределов |
| 413 | тело подписанного запроса больше 1 МБ |
| 429 | слишком много неверных токенов с адреса — подождать несколько минут |
Тело ответа — {"error": "…"} на языке из Accept-Language.
Исходящие вебхуки
Обратное направление: хаб сам шлёт события наружу — в n8n, чат-бот, свою систему. «Оповещения» → «Настройка» → «Исходящие вебхуки» → «Новый адресат»: имя, URL (http:// или https://), какие события, хосты и группы, язык текста. Хаб ходит наружу сам — открывать его не нужно.

События: не отвечает, снова отвечает, новые проблемы, проблемы устранены, задание не выполнено, перезагрузился, новые баны, выкладка прошла, выкладка не прошла. Вебхуки не зависят от записи в журнал: вид, который журнал не записывает, адресату всё равно придёт. Пределы по хостам и группам — только события своих хостов; выкладка — если все её хосты в пределах.
Запрос: POST, Content-Type: application/json, заголовки:
| Заголовок | Значение |
|---|---|
X-NKT-Event | вид события (unreachable, deploy-failed, test…) |
X-NKT-Delivery | номер доставки (повтор той же доставки — тот же номер) |
X-NKT-Timestamp | время, unix-секунды |
X-NKT-Signature | hex HMAC-SHA256 секретом от <timestamp>.<тело> |
Подпись — та же, что у входящих вебхуков nkt. Тело:
{
"id": "4f1c…", "kind": "unreachable", "ts": "2026-10-01T12:00:00Z",
"hub": "nkt 1.11.72", "text": "не отвечает: …", "severity": "warning",
"event_id": 120,
"host": {"id": 3, "name": "web1", "addr": "root@203.0.113.10:22"}
}У выкладки вместо host — pipeline (id, name), job_id, commit, tag, trigger и при неудаче error.
Доставка — в фоне; ответ не 2xx или нет связи — повтор через 10 с, 1 и 5 минут. Переадресации не выполняются. Итог последней доставки виден у адресата; «Проверить» шлёт событие test сразу и показывает ответ. Секрет подписи показывается один раз; «Новый секрет» — сразу другой. Правка — в окне с диффом, всё — в журнале действий.
Проверка в n8n (узел Webhook → Code):
const crypto = require('crypto')
const ts = $json.headers['x-nkt-timestamp']
const sig = crypto.createHmac('sha256', '<секрет>')
.update(ts + '.' + JSON.stringify($json.body)).digest('hex')
if (sig !== $json.headers['x-nkt-signature']) throw new Error('bad signature')Тело хаб отправляет без экранирования <, >, & — JSON.stringify разобранного тела даёт те же байты. Надёжнее всего — сырое тело (в узле Webhook n8n — «Raw Body»).