Netpulse_SasS/deploy/README.md
byrsapty 32442f8177
All checks were successful
CI / hygiene (push) Successful in 8s
CI / web (push) Successful in 1m4s
CI / server (push) Successful in 1m8s
CI / dbtest (push) Successful in 1m0s
CI / agent (push) Successful in 1m29s
Рішення власника: дзеркало конфігів лишається дослівним
Обрано побайтову точність архіву замість маскування — без неї
відновлення з архіву не працює. Наслідок прийнято свідомо: доступ до
дзеркала рівносильний доступу до всіх паролів у мережі.

Маскування лишається там, де конфіг читає людина: план відкату й
стенограми завдань.

Записано, щоб не переобговорювати.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 23:52:30 +03:00

522 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Розгортання NetPulse
Один хост, `docker compose`, автоматичний TLS. Такого розгортання
вистачає до кількох тисяч хостів на моніторингу; розносити служби по
машинах має сенс тоді, коли впирається БД, а не застосунок.
## Що з чого складається
| Служба | Роль | Порт |
| ----------- | ------------------------------------------------ | ----- |
| `db` | PostgreSQL 16 + TimescaleDB — усі дані | — |
| `cache` | DragonflyDB — черги й тимчасові стани | — |
| `migrate` | накочування схеми; відпрацьовує і зупиняється | — |
| `api` | HTTP API + вшитий інтерфейс | 8080 |
| `collector` | gRPC-колектор, до якого підключаються зонди | 9443 |
| `proxy` | Caddy: сертифікати, HTTPS, проксі до двох служб | 80/443/9443 |
Історія конфігів живе в Git на томі `git-data`, спільному для `api` й
`collector`: перший читає її для порівняння версій, другий пише під час
бекапу.
Назовні дивиться лише `proxy`. `api` і `collector` портів не публікують:
до них ходять через нього.
## Перший запуск
```sh
git clone <репозиторій> netpulse && cd netpulse
cp deploy/.env.example .env
```
Заповнити `.env`. Три значення обовʼязкові й генеруються так:
```sh
openssl rand -base64 24 # POSTGRES_PASSWORD
echo "np1=$(openssl rand -hex 32)" # NETPULSE_DEK
openssl rand -base64 48 # NETPULSE_JWT_SECRET
```
Далі:
```sh
docker compose up -d --build
docker compose run --rm --entrypoint netpulse-user cli \
-tenant default -login admin -role owner -name "Адміністратор"
```
Пароль команда спитає інтерактивно — щоб він не осів в історії оболонки
й у списку процесів.
Інтерфейс — на `https://<NETPULSE_DOMAIN>`.
## Пісочниця: повна установка, яку можна перевірити
Твердження «нова інсталяція піднімається сама» довго було доведене
міркуванням, а не запуском. Сухий прогін проходив, окремі кроки
перевірялись на живій базі — а повної установки з нуля не робив ніхто,
бо ніде: єдина доступна машина була бойовим стендом на 4 ГБ, і другий
повний стек поклав би робочу систему.
`./netpulse sandbox` ставить NetPulse **по-справжньому**, з тим самим
`docker-compose.yml`, тими самими міграціями й тією самою
самоперевіркою, але в окремому проєкті `compose` з власними томами й
портами на `127.0.0.1`. Це не імітація: вона або справді піднімає стек і
заходить у нього справжнім паролем, або зупиняється й каже, на чому.
```sh
./netpulse sandbox # підняти й лишити, щоб подивитись
./netpulse sandbox once # підняти, перевірити, знести все
./netpulse sandbox upgrade # другий режим: оновлення з версії на версію
./netpulse sandbox check # та сама перевірка ще раз
./netpulse sandbox status # що вона зараз займає
./netpulse sandbox down # знести все (обидва режими), включно з томами
./netpulse sandbox logs [служба] # журнали
```
Режимів два, і вони перевіряють різне: `sandbox` — установку з нуля,
`sandbox upgrade` — шлях «стояла стара версія, стала нова». Другий
описано нижче окремим розділом.
**Розробникові перед випуском**`./netpulse sandbox once`. Він робить
повну установку з нуля, проганяє всі твердження самоперевірки (вхід
справжнім паролем, кабінет назвався, сім переліків із міграцій
непорожні, дванадцять ендпоїнтів відповідають, зонд зареєструвався в
колекторі) і прибирає за собою до останнього тому. Ненульовий код
виходу означає, що цю збірку клієнтові віддавати не можна.
**Клієнтові — щоб подивитись до того, як ставити**`./netpulse
sandbox` без підкоманди. Після установки вона друкує адресу
`https://localhost:<порт>`, логін і пароль; система жива, у ній є
кабінет, локальний зонд і всі довідники. Подивились — `./netpulse
sandbox down`, і на машині не лишається нічого.
### Скільки це коштує
| Що | Скільки |
| -------------- | ------------------------------------------------------------ |
| Пам'ять у роботі | ~550900 МБ на всі шість служб |
| Стеля пам'яті | ~2.05 ГБ — жорстке обмеження з накладки, вище не підніметься |
| Пік при збірці | ще ~12 ГБ, поки збираються образи Go |
| Диск: томи | ~250 МБ (порожня база зі схемою, внутрішній CA Caddy, посвідчення зонда) |
| Диск: образи | ~2 ГБ, **спільні** з бойовою інсталяцією — якщо вони вже зібрані, пісочниця не додає нічого |
| Порти | 18080, 18081, 18082 (або наступна вільна трійка), усі на `127.0.0.1` |
Точні цифри після запуску показує `./netpulse sandbox status` — вони
виміряні, а не оцінені.
### Коли її запускати НЕ можна
Установник перевіряє це сам і **відмовляється**, а не пробує:
- **вільно менше 3 ГБ пам'яті** (4 ГБ, якщо образи ще треба зібрати).
Міряється `MemAvailable`, а не вся пам'ять: на машині, де вже працює
бойовий стек, «4 ГБ встановлено» не має жодного стосунку до того,
скільки з них можна взяти;
- **вільно менше 3 ГБ диска** (8 ГБ без готових образів);
- **на машині працює бойова інсталяція NetPulse.** Пісочниця не зіпсує
їй ані даних, ані портів — вона в іншому проєкті `compose`. Але
пам'ять і диск у них спільні, і два Postgres не вміщуються там, де
ледве вміщується один. Обійти можна прапорцем `--alongside`, і це
свідоме рішення, а не формальність;
- **docker compose старший за 2.24.4** — див. нижче;
- **пісочниця вже стоїть.** Друга поверх першої поділила б із нею томи,
і прогін «з нуля» перестав би бути прогоном з нуля.
Правильне місце для пісочниці — машина розробника або окрема віртуалка.
### Що буде при обриві
Пісочниця, яка лишила по собі том на 250 МБ і контейнер, що тримає
порт, — це та сама шкода, від якої вона мала захистити. Тому:
- `Ctrl-C`, `SIGTERM`, `SIGHUP` перехоплюються і прибирають усе;
- будь-яка зупинка установки (`ЗУПИНКА на кроці …`) теж прибирає все;
- у накладці стоїть `restart: "no"` — забута пісочниця **не воскресає**
після перезавантаження хоста;
- прибирання йде трьома ешелонами: `compose down -v`, потім пряме
`docker rm`/`docker volume rm` за міткою проєкту (ловить те, що
лишилось від обірваного `up`), потім видалення `.env.sandbox`;
- `--keep` лишає уламки для розбору журналів — але тільки при **невдачі
установки**, не при сигналі: перерваний прогін лишає по собі не стенд,
а половину стенду.
Чого перехопити неможливо: `kill -9` і зникнення живлення. Саме тому
джерелом правди про залишки є не файл-позначка, а сам docker — і
`./netpulse install` та `./netpulse check` при кожному запуску кажуть,
якщо на машині висить забута пісочниця.
### Чого пісочниця НЕ доводить
Це найважливіший абзац розділу. Зелена перевірка доводить тільки те, що
вона перевіряє — цей проєкт уже платив за протилежне припущення.
- **Let's Encrypt і DNS.** Пісочниця стоїть на `localhost` із
самопідписаним сертифікатом і навмисно не читає `netpulse.conf`:
справжній `DOMAIN` звідти відправив би її по сертифікат для адреси,
яка веде на бойовий стенд, і витрачені спроби списались би з тижневої
квоти домену.
- **Прийом SNMP-трапів і правило `DOCKER-USER`.** Порт 162/udp назовні
не виставляється взагалі: він не має автентифікації, і відкривати
його заради перевірки означало б купити перевірку ціною дірки.
- **Розрахунок `shared_buffers` із пам'яті хоста.** У пісочниці він
заданий числом — це умова того, щоб вона нічого не поклала.
- **Поведінка під навантаженням.** База порожня, хостів на моніторингу
немає, історії немає.
### Чому потрібен compose 2.24.4
Списки `ports` при накладанні compose-файлів **додаються**, а не
замінюються. Щоб зсунуті порти пісочниці не стали *додатком* до базових
80/443/9443, накладка перевизначає їх тегом `!override`, а він
з'явився у docker compose 2.24.4. На старішій версії пісочниця
відмовляється працювати — бо мовчки зайняти порти бойового проксі
гірше, ніж не запуститись.
Вимога знімається одним рядком у `docker-compose.yml`: якщо зробити
базові порти змінними —
```yaml
ports:
- "${NETPULSE_BIND:-0.0.0.0}:${NETPULSE_PORT_HTTP:-80}:80"
- "${NETPULSE_BIND:-0.0.0.0}:${NETPULSE_PORT_HTTPS:-443}:443"
- "${NETPULSE_BIND:-0.0.0.0}:${NETPULSE_PORT_GRPC:-9443}:9443"
```
— то пісочниці вистачить власного `.env.sandbox`, накладка портів стане
непотрібною, а мінімальна версія compose лишиться 2.0, як у решті
установника.
## Пісочниця, режим другий: оновлення з версії на версію
Установку з нуля клієнт робить один раз, а оновлення — щоразу. І
ламається воно частіше: міграції котяться не на порожню базу, а на ту, де
вже лежать чужі дані; конфігурація змінюється; зонд у мережі клієнта
лишається старим, а колектор стає новим. Досі цього не перевіряло нічого,
тобто перевіряв перший клієнт.
```sh
./netpulse sandbox upgrade # попередня версія обереться сама
./netpulse sandbox upgrade --from REF # попередня версія — цей коміт або тег
./netpulse sandbox upgrade --keep # лишити стенд після прогону
```
Прогін іде п'ятьма етапами, і кожен друкує свою назву — падіння називає
етап, а не лише крок:
| Етап | Що відбувається |
| ---- | --------------- |
| **0** | вибір попередньої версії й розгортання її дерева |
| **А** | попередня версія ставиться **з нуля** тим самим `install` і проходить ту саму самоперевірку |
| **Б** | у базу наливаються локації, 24 хости, доба ICMP і метрик із кроком 5 хв, конфіги NCM, тригер і алерти; знімаються кількості «до» |
| **В** | дамп, збірка нової версії, **міграції поверх наявних даних**, перезапуск служб, перевірка старого зонда проти нового колектора, оновлення зонда |
| **Г** | та сама `selfcheck`, що й після чистої установки |
| **Д** | кількості «після» проти «до», по рядках |
За замовчуванням стенд зноситься разом із томами: це ворота випуску, а не
стенд для розглядання. `--keep` лишає все на місці — і тоді прибрати
**обов'язково**: `./netpulse sandbox down`.
### Що таке «попередня версія», якщо версіонування ще немає
Питання не риторичне: від відповіді залежить, чого вартий прогін. Тегів
немає, реєстру немає, образ завжди `netpulse/server:dev`.
**Обрано коміт git.** Коміт — це повне дерево: Go-код, міграції,
`docker-compose.yml`, `Caddyfile`, `Dockerfile`. З нього збираються
справжні старі образи, тобто попередня версія тут не описана, а
**виконується**. Це відтворювано: той самий ref дасть той самий стенд і
за півроку.
І це не тимчасове рішення. Тег у git — теж ref, тому в день першого тегу
тут не зміниться жодного рядка: `--from v0.1.0` запрацює сам.
Чому не інакше:
* **Тег.** Найправильніше — і неможливе сьогодні: перевірка оновлення є
вхідним квитком *до* першого тегу (ROADMAP, Етап 13). Вимагати тег
означало б вимагати те, заради чого вона й пишеться.
* **Збережений дамп бази.** Найдешевше й доводить найменше. Дамп — це
схема з даними, але не бінарники: він не запускає старий колектор і
старий зонд, тобто не бачить двох названих класів поломки з трьох.
Гірше інше: дамп старіє мовчки. Його зробила версія, яку вже ніхто не
збере, і коли прогін почервоніє, розрізнити «зламався код» і «протух
дамп» буде нічим.
Типовий ref обчислюється, а не вписаний числом: береться найновіший
коміт, у якого міграцій **менше**, ніж у HEAD. Причина та сама, що й у
решті установника: зелений прогін, який нічого не перевірив, гірший за
відсутність прогону. Оновлення без жодної нової міграції доводить лише
те, що служби перезапустились, — і мовчки видається за доказ, що міграції
котяться поверх даних. Якщо в парі версій нових міграцій немає, прогін
про це кричить — і в момент вибору, і в підсумку.
**Перед випуском беріть `--from` явно** — із тією версією, з якої
клієнти оновлюватимуться насправді. Автоматичний вибір — це «найближча
попередня», а не «та, що справді стоїть».
### Ізоляція: чому це не може зачепити нічого чужого
| Що | Як розведено |
| -- | ------------ |
| Проєкт compose | `netpulse-sandbox-upgrade` — окремі контейнери, мережа й **томи** |
| Порти | від **18300** на `127.0.0.1`; режим установки з нуля перебирає 18080…18272 і сюди не дістає |
| `.env` | власний `.env.sandbox-upgrade`; бойовий `.env` не читається й не пишеться |
| `netpulse.conf` | не читається взагалі (див. `read_conf`) |
| Образи | власні теги `netpulse/*:sandbox-prev` і `:sandbox-new`; спільний тег `:dev` не чіпається жодного разу |
| Файрвол хоста | правило `DOCKER-USER` не додається: `TRAPS_FROM` порожній |
| Репозиторій | дерево розгортається через `git archive` — він **лише читає**; ані `worktree`, ані `checkout` |
Окремі теги образів — не дрібниця. Без них обидві версії називались би
`netpulse/server:dev`, друга перетерла б першу, а обірваний прогін лишив
би цей тег указувати на **стару** збірку — на очах у бойової інсталяції,
яка ділить із пісочницею реєстр образів.
Дерево попередньої версії лежить у `.sandbox-prev/` поруч із проєктом (не
в `/tmp`: там часто tmpfs, і другий примірник вихідних текстів ліг би в
оперативну пам'ять). З контексту збірки його виключає `.dockerignore`.
`./netpulse sandbox down` прибирає **обидва** режими одразу — контейнери,
томи, мережу, `.env`, дерево й власні теги образів. Пам'ятати, у якому
режимі стенд піднімали, не треба.
### Що саме звіряється в кінці
Дві різні вимоги, тому й два правила:
* **`exact`** — рядки, які налила сама пісочниця (хости `sb-sw-*`, їхні
проби, ряди метрик, конфіги, тригер, алерти). Їхня кількість не має
права змінитись **ані в який бік**: поменшало — оновлення знищило,
побільшало — роздвоїло. Обидва однаково погані й обидва беззвучні.
* **`min`** — усе інше (кабінети, користувачі, довідники, аудит). Тут
дозволено лише рости: система під час оновлення жива.
Кожен `exact`-запит звужено до рядків пісочниці, бо локальний зонд весь
цей час пише свою телеметрію. Алерти рахуються по `alr.alerts` **і**
`alr.alerts_history` одразу: фоновий такт переносить погашені в історію,
і рядок, що переїхав, — це не втрачений рядок.
### Чого цей режим НЕ доводить
* **Гілку «наявна інсталяція» в міграторі.** База пісочниці народжується
чистою, тому `public.netpulse_install.fresh` назавжди `true`, і мігратор
іде гілкою «видати паролі ролям». Перехід стенду, зробленого до 0063
(`deploy/RLS-EXISTING-INSTALL.md`), лишається неперевіреним.
* **Зміни в самому установнику між версіями.** Обидва етапи веде один і
той самий `./netpulse` — вимірювальний прилад має бути тим самим на
обох кінцях вимірювання, інакше різниця в приладі читається як різниця
у виробі. Під перевіркою тут те, що установник ставить, а не він сам.
* **Оновлення через кілька версій підряд.** Перевіряється рівно один
стрибок: з обраного ref у HEAD.
* Усе те, чого не доводить і перший режим: Let's Encrypt і DNS, трапи на
162/udp, поведінка під навантаженням.
## Підключення зонда
В інтерфейсі: **Зонди → Додати зонд**. Видане запрошення (`np_enr_…`)
одноразове — після реєстрації воно згоряє.
На машині, де стоятиме зонд:
```sh
docker run -d --name netpulse-agent --restart unless-stopped \
--cap-add NET_RAW \
-v netpulse-agent:/var/lib/netpulse \
netpulse/agent:dev \
-server netpulse.example.com:9443 \
-enroll np_enr_… \
-name "Зонд у Львові" \
-modules icmp,snmp,topology,ncm
```
Зонд обміняє запрошення на постійний токен і збереже його в
`/var/lib/netpulse/agent.json` (права 0600). Наступні запуски токена вже
не потребують — том із посвідченням має пережити перестворення
контейнера, інакше кожен старт вимагатиме нового запрошення.
Усі зʼєднання зонда вихідні: у мережі клієнта не треба відкривати
жодного порту.
## Бекап
Три речі, і всі три обовʼязкові:
1. **База**усе, крім секретів у відкритому вигляді.
2. **`NETPULSE_DEK`** — без нього паролі SSH і SNMP-community з бекапу не
розшифрувати. У БД лежить лише шифротекст.
3. **`NETPULSE_JWT_SECRET`** — без нього після відновлення всі активні
сесії відваляться. Не смертельно, але користувачі помітять.
```sh
docker compose exec -T db \
pg_dump -U netpulse -d netpulse -Fc --no-owner \
> netpulse-$(date +%F).dump
```
Формат `-Fc` (custom), а не простий SQL: він стискається і дозволяє
відновлювати вибірково.
Ключі зберігати **окремо від дампа** — інакше сенс шифрування секретів
зникає: той, хто дістав бекап, дістав і ключ до нього.
Том `git-data` бекапити не обовʼязково: тіла конфігів лежать
зашифрованими в базі, і репозиторій повністю відтворюється з неї —
```sh
docker compose run --rm --entrypoint netpulse-gitsync api
```
Зворотне невірно: з репозиторію базу не відновити. Тому джерелом істини
лишається дамп, а Git — похідне сховище, яке коштує один запуск команди.
### Автоматично, щодня
```cron
15 3 * * * cd /opt/netpulse && docker compose exec -T db pg_dump -U netpulse -d netpulse -Fc --no-owner > /var/backups/netpulse-$(date +\%F).dump && find /var/backups -name 'netpulse-*.dump' -mtime +30 -delete
```
## Відновлення
TimescaleDB вимагає рамки навколо відновлення: без неї фонові процеси
агрегації втручаються в наливання даних і дамп лягає пошкодженим.
```sh
docker compose stop api collector
docker compose exec -T db psql -U netpulse -d postgres -c \
'DROP DATABASE IF EXISTS netpulse; CREATE DATABASE netpulse;'
docker compose exec -T db psql -U netpulse -d netpulse -c \
'CREATE EXTENSION IF NOT EXISTS timescaledb;'
docker compose exec -T db psql -U netpulse -d netpulse -c \
'SELECT timescaledb_pre_restore();'
docker compose exec -T db pg_restore -U netpulse -d netpulse --no-owner \
< netpulse-2026-08-25.dump
docker compose exec -T db psql -U netpulse -d netpulse -c \
'SELECT timescaledb_post_restore();'
docker compose up -d api collector
```
У `.env` має лежати **той самий** `NETPULSE_DEK`, що й на момент дампа.
Інакше застосунок підніметься, але кожна спроба скористатись збереженим
паролем поверне помилку розшифрування — і виглядатиме це як зламані
креденшели, а не як втрачений ключ.
Перевірка після відновлення:
```sh
docker compose exec -T db psql -U netpulse -d netpulse -c \
'SELECT count(*) FROM core.devices;'
curl -sf https://<NETPULSE_DOMAIN>/healthz && echo OK
```
## Оновлення
```sh
git pull
docker compose up -d --build
```
`migrate` відпрацює першим і не дасть піднятись API, якщо схема не
накотилась. Міграції йдуть по одній у транзакції; уже застосований файл
зі зміненою контрольною сумою зупиняє весь запуск — це захист від
мовчазного розходження схеми з кодом.
Відкат схеми не передбачений: зворотні міграції на даних телеметрії
коштують дорожче, ніж відновлення з бекапу.
Перед тим як віддавати оновлення клієнтові, цей самий шлях треба
прогнати в ізоляції: `./netpulse sandbox upgrade --from <версія, що
стоїть у клієнта>`. Він ставить стару версію з нуля, наливає в неї дані,
оновлює й окремо звіряє, що дані пережили. Розділ «Пісочниця, режим
другий» вище.
## Ізоляція кабінетів (RLS)
**Нова інсталяція вже під політиками — робити нічого не треба.**
`netpulse-migrate` на чистій базі сам видає паролі ролям `netpulse_app`
і `netpulse_worker`, і застосунок з першої секунди ходить роллю без
`BYPASSRLS`.
Ізоляція тримається на двох незалежних рубежах: політика RLS у базі й
предикат `tenant_id` у кожному запиті коду. Другий потрібен окремо, бо
на гіпертаблицях RLS не працює взагалі — TimescaleDB не поєднує його зі
стисненням, а туди йде вся телеметрія.
**Інсталяціям, старшим за 0063,** застосунок і далі ходить роллю
`netpulse` — тобто суперкористувачем, який політики обходить, — і другий
рубіж вмикається окремою оборотною процедурою:
**deploy/RLS-EXISTING-INSTALL.md**. Вона не змінює даних. Робити її
разом з оновленням версії не варто: ламатись у них різне, і розбирати
доведеться одночасно.
Свій випадок видно одним запитом:
```sh
docker compose exec -T db psql -U netpulse -d netpulse -c "SELECT fresh FROM public.netpulse_install"
```
## Зміна ключа шифрування
Ключі перелічуються через кому, новий — першим:
```
NETPULSE_DEK=np2=<новий hex>,np1=<старий hex>
```
Нові секрети шифруються першим ключем, старі читаються своїм. Прибирати
старий ключ можна лише після того, як усі секрети перезаписані.
## Чому саме так
**Два образи, а не пʼять.** `api`, `collector`, `migrate`, `netpulse-user`
і `netpulse-secret` — з одного модуля, з половиною спільного коду. Один
образ гарантує, що API і колектор ходять у схему БД однією версією; окремі
образи дають їм можливість розʼїхатись саме там, де це найдорожче.
Зонд — окремо: він їде в чужу мережу, і DSN, ключі шифрування та команди
заведення користувачів не повинні бути в тому образі навіть як
невикористані файли.
**Міграції окремою службою.** API піднімається в кількох примірниках;
накочування схеми зі старту означало б гонку між ними.
**TLS на проксі, а не в застосунку.** Прострочений сертифікат на системі,
яка сама має повідомляти про проблеми, — найгірший спосіб дізнатись про
проблему. Caddy оновлює його сам.
## Без домену
Домен потрібен для справжнього сертифіката: Let's Encrypt не видає їх на
IP-адреси. Поки домену немає, лишіть `ACME_EMAIL` порожнім — Caddy
випише самопідписаний, і система працюватиме одразу, з попередженням у
браузері.
Коли домен зʼявиться: замінити `NETPULSE_DOMAIN`, вписати `ACME_EMAIL` і
`docker compose restart proxy`. Caddyfile змонтований, тож перезбирати
образи не треба — але й `up -d` без `restart` його не перечитає.
## Дашборд на телевізор
В інтерфейсі: **Дашборд → На телевізор → Видати посилання**. Отриману
адресу відкривають на екрані в диспетчерській — вона не потребує входу.
Телевізор нікуди не залогиниш: сесія протермінується, браузер оновиться,
і зранку на стіні висітиме форма входу замість карти мережі — рівно
тоді, коли на неї дивляться.
Що дає посилання і чого не дає:
- **тільки читання** цього дашборда: його плитки, активні алерти й
метрики тих хостів, які на ньому показані;
- метрики чужого хоста за ним не дістати навіть підбором ідентифікатора;
- решта кабінету — інвентар, конфіги, налаштування, секрети —
недоступна;
- відкликається одним рухом, старе посилання одразу мертве.
Токен показується один раз. Видати нове можна будь-коли — попереднє при
цьому перестає працювати.