П'ять паралельних задач. Найцінніше в них — не можливості, а знайдене.
0069 БІЛІНГ. Аудит 0009 показав, що перевірка ліміту не спрацювала б
жодного разу: isPlanLimit шукала слово «ліміт», а тригер писав
"device limit reached" англійською. Перше ж досягнення стелі дало б
клієнту 500 замість пояснення. Плюс три діри: тригер лише на INSERT
(стеля в 15 обходилась за чотири дії через архів), max_maps/max_agents/
max_users не перевіряло ніщо — тобто рівно те, чим відрізняються плани,
і license_keys була закрита політикою tenant_isolation з 0011, хоча
tenant_id там NULLABLE навмисно: головний сценарій self-hosted був
недосяжний.
Після закінчення ліцензії не вимикається нічого — замерзає лише ріст.
Моніторинг, що перестав моніторити через несплачений рахунок, це
аварія в мережі клієнта, спричинена нами.
0070 SLA. Джерелом обрано ts.icmp_1h, а не device_status_history:
остання не вміє сказати «ми не знали» — перехід пишеться лише при
зміні стану, тож доба мовчання зонда виглядає як доба роботи. Час
розкладено на чотири частини, і «немає даних» не додається ні до чого;
замість вибору між двома брехнями звіт каже, яку частку періоду він
бачив. Закритий період тримає тригер, а не домовленість у Go.
0071 ВІДПОВІДНІСТЬ. 20 правил, кожне прив'язане до родини: об'єднаний
вираз, що покриває Cisco й не покриває MikroTik, дав би «0 порушень» і
сховав сліпу пляму. Вендор не входить у перелік, доки для нього немає
зразка конфігу в тесті. TestBuiltinRulesAreNotAlwaysGreen вимагає, щоб
у кожного правила був конфіг, де воно спрацювало, І де ні.
ПІСОЧНИЦЯ УСТАНОВНИКА — та сама установка в ізольованому проєкті
compose. Знайшла дві справжні вади з трьох спроб:
* healthcheck бази ходив unix-сокетом, а споживачі по TCP. При
первинній ініціалізації Postgres слухає лише сокет — compose
вважав базу здоровою, migrate отримував connection refused. На
створеній базі цієї фази немає, тож вада чекала на першого клієнта;
* у білому переліку модулів API не було traps і filecfg — зонд із
приймачем трапів неможливо було зареєструвати взагалі.
ТЕСТИ СТОРІНОК: 137 → 252. Мережевий шар, права доступу, незворотні
дії, фільтри з адресного рядка. Підмінюється лише fetch і WebSocket —
api/client.ts працює справжній.
|
||
|---|---|---|
| .. | ||
| .env.example | ||
| act-runner.config.yml | ||
| Caddyfile | ||
| docker-compose.ci.yml | ||
| docker-compose.sandbox.yml | ||
| Dockerfile.agent | ||
| Dockerfile.server | ||
| files.conf.example | ||
| README.md | ||
| RLS-EXISTING-INSTALL.md | ||
Розгортання 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 портів не публікують:
до них ходять через нього.
Перший запуск
git clone <репозиторій> netpulse && cd netpulse
cp deploy/.env.example .env
Заповнити .env. Три значення обовʼязкові й генеруються так:
openssl rand -base64 24 # POSTGRES_PASSWORD
echo "np1=$(openssl rand -hex 32)" # NETPULSE_DEK
openssl rand -base64 48 # NETPULSE_JWT_SECRET
Далі:
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. Це не імітація: вона або справді піднімає стек і
заходить у нього справжнім паролем, або зупиняється й каже, на чому.
./netpulse sandbox # підняти й лишити, щоб подивитись
./netpulse sandbox once # підняти, перевірити, знести все
./netpulse sandbox check # та сама перевірка ще раз
./netpulse sandbox status # що вона зараз займає
./netpulse sandbox down # знести все, включно з томами
./netpulse sandbox logs [служба] # журнали
Розробникові перед випуском — ./netpulse sandbox once. Він робить
повну установку з нуля, проганяє всі твердження самоперевірки (вхід
справжнім паролем, кабінет назвався, сім переліків із міграцій
непорожні, дванадцять ендпоїнтів відповідають, зонд зареєструвався в
колекторі) і прибирає за собою до останнього тому. Ненульовий код
виходу означає, що цю збірку клієнтові віддавати не можна.
Клієнтові — щоб подивитись до того, як ставити — ./netpulse sandbox без підкоманди. Після установки вона друкує адресу
https://localhost:<порт>, логін і пароль; система жива, у ній є
кабінет, локальний зонд і всі довідники. Подивились — ./netpulse sandbox down, і на машині не лишається нічого.
Скільки це коштує
| Що | Скільки |
|---|---|
| Пам'ять у роботі | ~550–900 МБ на всі шість служб |
| Стеля пам'яті | ~2.05 ГБ — жорстке обмеження з накладки, вище не підніметься |
| Пік при збірці | ще ~1–2 ГБ, поки збираються образи 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: якщо зробити
базові порти змінними —
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, як у решті
установника.
Підключення зонда
В інтерфейсі: Зонди → Додати зонд. Видане запрошення (np_enr_…)
одноразове — після реєстрації воно згоряє.
На машині, де стоятиме зонд:
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). Наступні запуски токена вже
не потребують — том із посвідченням має пережити перестворення
контейнера, інакше кожен старт вимагатиме нового запрошення.
Усі зʼєднання зонда вихідні: у мережі клієнта не треба відкривати жодного порту.
Бекап
Три речі, і всі три обовʼязкові:
- База — усе, крім секретів у відкритому вигляді.
NETPULSE_DEK— без нього паролі SSH і SNMP-community з бекапу не розшифрувати. У БД лежить лише шифротекст.NETPULSE_JWT_SECRET— без нього після відновлення всі активні сесії відваляться. Не смертельно, але користувачі помітять.
docker compose exec -T db \
pg_dump -U netpulse -d netpulse -Fc --no-owner \
> netpulse-$(date +%F).dump
Формат -Fc (custom), а не простий SQL: він стискається і дозволяє
відновлювати вибірково.
Ключі зберігати окремо від дампа — інакше сенс шифрування секретів зникає: той, хто дістав бекап, дістав і ключ до нього.
Том git-data бекапити не обовʼязково: тіла конфігів лежать
зашифрованими в базі, і репозиторій повністю відтворюється з неї —
docker compose run --rm --entrypoint netpulse-gitsync api
Зворотне невірно: з репозиторію базу не відновити. Тому джерелом істини лишається дамп, а Git — похідне сховище, яке коштує один запуск команди.
Автоматично, щодня
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 вимагає рамки навколо відновлення: без неї фонові процеси агрегації втручаються в наливання даних і дамп лягає пошкодженим.
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, що й на момент дампа.
Інакше застосунок підніметься, але кожна спроба скористатись збереженим
паролем поверне помилку розшифрування — і виглядатиме це як зламані
креденшели, а не як втрачений ключ.
Перевірка після відновлення:
docker compose exec -T db psql -U netpulse -d netpulse -c \
'SELECT count(*) FROM core.devices;'
curl -sf https://<NETPULSE_DOMAIN>/healthz && echo OK
Оновлення
git pull
docker compose up -d --build
migrate відпрацює першим і не дасть піднятись API, якщо схема не
накотилась. Міграції йдуть по одній у транзакції; уже застосований файл
зі зміненою контрольною сумою зупиняє весь запуск — це захист від
мовчазного розходження схеми з кодом.
Відкат схеми не передбачений: зворотні міграції на даних телеметрії коштують дорожче, ніж відновлення з бекапу.
Ізоляція кабінетів (RLS)
Нова інсталяція вже під політиками — робити нічого не треба.
netpulse-migrate на чистій базі сам видає паролі ролям netpulse_app
і netpulse_worker, і застосунок з першої секунди ходить роллю без
BYPASSRLS.
Ізоляція тримається на двох незалежних рубежах: політика RLS у базі й
предикат tenant_id у кожному запиті коду. Другий потрібен окремо, бо
на гіпертаблицях RLS не працює взагалі — TimescaleDB не поєднує його зі
стисненням, а туди йде вся телеметрія.
Інсталяціям, старшим за 0063, застосунок і далі ходить роллю
netpulse — тобто суперкористувачем, який політики обходить, — і другий
рубіж вмикається окремою оборотною процедурою:
deploy/RLS-EXISTING-INSTALL.md. Вона не змінює даних. Робити її
разом з оновленням версії не варто: ламатись у них різне, і розбирати
доведеться одночасно.
Свій випадок видно одним запитом:
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 його не перечитає.
Дашборд на телевізор
В інтерфейсі: Дашборд → На телевізор → Видати посилання. Отриману адресу відкривають на екрані в диспетчерській — вона не потребує входу.
Телевізор нікуди не залогиниш: сесія протермінується, браузер оновиться, і зранку на стіні висітиме форма входу замість карти мережі — рівно тоді, коли на неї дивляться.
Що дає посилання і чого не дає:
- тільки читання цього дашборда: його плитки, активні алерти й метрики тих хостів, які на ньому показані;
- метрики чужого хоста за ним не дістати навіть підбором ідентифікатора;
- решта кабінету — інвентар, конфіги, налаштування, секрети — недоступна;
- відкликається одним рухом, старе посилання одразу мертве.
Токен показується один раз. Видати нове можна будь-коли — попереднє при цьому перестає працювати.