feat(license): сервер лицензий lic.zetit.ru — кабинет, онлайн-активация и защита

- bj-license-server: активация по коду и продление токенов (API /v1), кабинет
  администратора (клиенты, лицензии, установки, журнал, администраторы)
- защита: доверенные IP установок, jail fail2ban api/admin, раздел «Защита»
  с режимом обороны, белым списком и сроками блокировок (root-помощник fw-apply)
- доступ к кабинету: свой адрес вместо /admin, вход только из белого списка,
  второй фактор — TOTP или подтверждение в «ЗЕТИТ Ключ» (push/код)
- deploy/license: unit-файлы, fail2ban, path/timer помощника; контракт API в docs

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-15 19:41:11 +03:00
parent 586ffb3a31
commit 86aa452ed6
19 changed files with 4659 additions and 99 deletions
+82
View File
@@ -0,0 +1,82 @@
# Сервер лицензирования и обновлений bj — контракт API
Сервер: **https://lic.zetit.ru** (ВМ 46.173.19.220, Ubuntu 26.04). Адрес зашит в bj
(`internal/lkgateway`, константа), в интерфейсе не меняется. С этого же сервера bj
получает обновления.
## Модель
- Лицензия — прежний токен `internal/license`: подписанный Ed25519 JSON `License`,
строка `base64(payload).base64(sig).keyid`. Ключ подписи — `~/keys/license.priv`
(keyid `main`); открытый ключ зашит в bj.
- Клиенту выдаётся **код активации** (`BJ-XXXX-XXXX-XXXX`), один на лицензию; им
активируется до `max_nodes` установок (0 — без лимита).
- При активации сервер выдаёт токен **на 60 дней** (`expires_at`), но не дольше
`contract_until`; в токене `install_id` установки и `contract_until` договора.
- bj продлевает токен **раз в сутки**. Отзыв = сервер больше не продлевает; уже
выданный токен доживает свой срок (запас на недоступность сервера).
- Офлайн-ключи (`bj-license issue`, без `install_id`) продолжают работать — для
закрытых контуров без интернета.
## Идентификатор установки
`install_id` — UUID v4, создаётся bj при первом запуске и хранится в
`/var/lib/bj/.bj/setup.json` (`license.install_id`). Вместе с ним bj передаёт
`machine` — sha256(`/etc/machine-id`) в hex (для учёта и обнаружения клонов, не для
отказа).
## Запросы
Все тела — JSON, `Content-Type: application/json`. Ошибки — HTTP 4xx/5xx и
`{"error": "<код>", "message": "<текст по-русски>"}`.
### POST /v1/activate
```json
{"code": "BJ-7K3M-Q9XP-2HDA", "install_id": "…uuid…", "machine": "…sha256…",
"hostname": "bj-server-01", "version": "0.1.0"}
```
200:
```json
{"token": "<payload.sig.main>", "license_id": "…uuid…", "tenant": "ИК Фонтвьель",
"expires_at": "2026-11-14T10:00:00Z", "contract_until": "2027-09-14T00:00:00Z",
"renew_after_sec": 86400}
```
Коды ошибок: `bad_request`, `unknown_code`, `revoked`, `contract_expired`,
`max_nodes` (лимит установок исчерпан). Повторная активация той же `install_id` —
не новая установка, просто новый токен.
### POST /v1/renew
```json
{"token": "<текущий токен>", "install_id": "…", "machine": "…", "hostname": "…", "version": "…"}
```
Сервер проверяет подпись токена (срок **не** проверяет — продлить можно и
истёкший), совпадение `install_id` с токеном, что установка известна и лицензия не
отозвана. 200 — тот же формат, что у activate. Ошибки: `bad_token`,
`install_mismatch`, `unknown_install`, `revoked`, `contract_expired`.
Каждый успешный renew обновляет `last_seen`, `version`, `hostname`, IP установки.
### Обновления
- `GET /v1/{channel}/manifest.json` — подписанный манифест (`internal/release`),
без авторизации.
- `GET /v1/{channel}/files/{file}` — только с заголовком
`Authorization: License <token>`: подпись верна, срок не истёк, лицензия не
отозвана, в признаках есть `updates` (или план `enterprise`). Иначе 401/403 с
JSON-ошибкой. Ключ подписи манифестов отдельный от лицензионного.
### GET /healthz
`ok`.
## Кабинет администратора
`https://lic.zetit.ru/admin/` — вход по логину и паролю (bcrypt), только для нас:
клиенты (название, ИНН), лицензии (план, признаки, max_nodes, contract_until, код
активации, отзыв/восстановление), установки (install_id, hostname, версия, IP,
последнее продление, срок токена), журнал событий, выпуск офлайн-ключа для
закрытого контура.