diff --git a/HISTORY.md b/HISTORY.md index dba3940..480ba5a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -7844,3 +7844,21 @@ git-дзеркало ДОСЛІВНО. Маскувати їх означає з Заодно з'ясувалось, що ROADMAP брехав: CI не «чекає на раннера» — він працює весь день, задачі 85–111. На коміт c83324a пройшли всі п'ять робіт: hygiene, web, server, dbtest, agent. + +### Рішення власника: дзеркало лишається дослівним (2026-08-28) + +Питання стояло так: тіла конфігів ідуть у git-дзеркало точнісінько +такими, як на пристрої, — разом із секретами, які в них є. Маскувати +означало б зламати відновлення з архіву, бо цінність NCM саме в +побайтовій точності. + +**Обрано точність.** Дзеркало пише конфіг як є; git-сервер вважається +довіреним. Пароль скомпрометованого облікового запису власник змінює сам. + +Що це означає надалі, щоб не переобговорювати: + +* маскування діє там, де конфіг читає ЛЮДИНА (план відкату, стенограми + завдань) — і саме там сьогодні залатали дірку з відлунням пароля; +* архів і дзеркало НЕ маскуються навмисно; +* отже, доступ до дзеркала треба вважати рівносильним доступу до всіх + паролів у мережі. Це не побічний ефект, а свідомо прийнята ціна. diff --git a/ROADMAP.md b/ROADMAP.md index 88dc5a3..4944f70 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -633,6 +633,14 @@ S3. Приблизно пів дня. (`gofmt`, `go vet`, тести з базою, крос-збірка зонда), проганялось окремо перед кожним комітом. +### Вирішено й закрито + +**Дзеркало конфігів лишається дослівним** (рішення власника, 2026-08-28). +Секрети з конфігів їдуть у git-дзеркало незамаскованими — обрано +побайтову точність архіву, бо без неї відновлення з нього не працює. +Наслідок прийнято свідомо: доступ до дзеркала рівносильний доступу до +всіх паролів у мережі. Маскування лишається там, де конфіг читає людина. + ### Чого не буде без зовнішніх умов - **Справжній TLS** на бойовому сервері — потрібен домен. Зараз diff --git a/deploy/README.md b/deploy/README.md index 3806543..96c987a 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -67,12 +67,17 @@ docker compose run --rm --entrypoint netpulse-user cli \ ```sh ./netpulse sandbox # підняти й лишити, щоб подивитись ./netpulse sandbox once # підняти, перевірити, знести все +./netpulse sandbox upgrade # другий режим: оновлення з версії на версію ./netpulse sandbox check # та сама перевірка ще раз ./netpulse sandbox status # що вона зараз займає -./netpulse sandbox down # знести все, включно з томами +./netpulse sandbox down # знести все (обидва режими), включно з томами ./netpulse sandbox logs [служба] # журнали ``` +Режимів два, і вони перевіряють різне: `sandbox` — установку з нуля, +`sandbox upgrade` — шлях «стояла стара версія, стала нова». Другий +описано нижче окремим розділом. + **Розробникові перед випуском** — `./netpulse sandbox once`. Він робить повну установку з нуля, проганяє всі твердження самоперевірки (вхід справжнім паролем, кабінет назвався, сім переліків із міграцій @@ -182,6 +187,130 @@ sandbox down`, і на машині не лишається нічого. непотрібною, а мінімальна версія 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_…`) @@ -300,6 +429,12 @@ docker compose up -d --build Відкат схеми не передбачений: зворотні міграції на даних телеметрії коштують дорожче, ніж відновлення з бекапу. +Перед тим як віддавати оновлення клієнтові, цей самий шлях треба +прогнати в ізоляції: `./netpulse sandbox upgrade --from <версія, що +стоїть у клієнта>`. Він ставить стару версію з нуля, наливає в неї дані, +оновлює й окремо звіряє, що дані пережили. Розділ «Пісочниця, режим +другий» вище. + ## Ізоляція кабінетів (RLS) **Нова інсталяція вже під політиками — робити нічого не треба.** diff --git a/netpulse b/netpulse index c248a52..2d86554 100644 --- a/netpulse +++ b/netpulse @@ -2831,11 +2831,16 @@ BEGIN WHERE d.tenant_id = v_tenant AND d.name LIKE 'sb-sw-%' ON CONFLICT DO NOTHING; + -- Звуження до наших рядів обов'язкове, а не для охайності: локальний + -- зонд до цієї миті вже працює й міг завести власний ряд cpu.util. + -- Писати в чужий ряд означало б підмішати наші відліки до справжніх. INSERT INTO ts.samples (ts, series_id, value) SELECT g, s.id, 30.0 - FROM ts.series s, + FROM ts.series s + JOIN inv.devices d ON d.id = s.device_id, generate_series(now() - interval '24 hours', now(), interval '5 minutes') AS g WHERE s.tenant_id = v_tenant AND s.metric_key = 'cpu.util' + AND d.name LIKE 'sb-sw-%' ON CONFLICT DO NOTHING; -- Конфіги. Тіло живе в Git, тут метадані — але саме їх і чіпають @@ -2854,12 +2859,14 @@ BEGIN -- Алерти лише на частині хостів: інакше «усі рядки однакові», і -- міграція, яка псує вибірку за станом, лишилась би непоміченою. - -- Мітка в context, а не впізнавання за назвою. Причина конкретна: - -- фоновий такт цілком може або погасити ці алерти й перенести їх у - -- alr.alerts_history (звідки dedup_key не переїжджає взагалі), або - -- завести СВОЇ алерти за тим самим правилом і з тією самою назвою. - -- І перше, і друге зсунуло б лічильник, і звірка «до й після» дала б - -- хибне червоне — тобто збрехала б рівно про те, заради чого існує. + -- + -- Мітка в context, а не впізнавання за назвою чи dedup_key. Причина + -- конкретна: фоновий такт цілком може або погасити ці алерти й + -- перенести їх у alr.alerts_history (куди dedup_key не переїжджає + -- взагалі), або завести СВОЇ алерти за тим самим правилом і з тією + -- самою назвою. І перше, і друге зсунуло б лічильник, і звірка «до й + -- після» дала б хибне червоне — тобто збрехала б рівно про те, заради + -- чого існує. context переїжджає в історію разом із рядком. INSERT INTO alr.alerts (tenant_id, rule_id, device_id, severity, state, title, message, dedup_key, value, threshold, context) @@ -2889,6 +2896,12 @@ SQL # оновлення жива, локальний зонд пише телеметрію, дії лягають # в аудит. Вимагати тут рівності означало б отримати хибне # червоне на кожному другому прогоні. +# +# Кожен exact-запит навмисно звужений до рядків, які налила саме +# пісочниця: локальний зонд весь цей час пише СВОЮ телеметрію, і без +# звуження вона потрапила б у ті самі лічильники. Алерти рахуються по +# ДВОХ таблицях одразу — фоновий такт переносить погашені в +# alr.alerts_history, і рядок, що переїхав, це не втрачений рядок. sandbox_up_probes() { cat <<'EOF' хости_пісочниці|exact|SELECT count(*) FROM inv.devices WHERE name LIKE 'sb-sw-%' @@ -2969,6 +2982,11 @@ sandbox_up_compare() { "" \ "$(printf '%s' "$_bad" | tr '|' '\n')" \ "" \ + "Рядок «поменшало там, де можуть лише прибувати» має один законний" \ + "привід: міграція свідомо прибрала запис із довідника (застаріле" \ + "право, зайвий тип перевірки). Тоді це не поломка — але сказати про" \ + "це має людина, а не мовчазний зелений підсумок." \ + "" \ "Дамп бази ПЕРЕД оновленням лежить у $SB_UP_TREE/before-upgrade.dump" \ "(з --keep він переживе цей прогін)." fi