Рішення власника: дзеркало конфігів лишається дослівним
Обрано побайтову точність архіву замість маскування — без неї відновлення з архіву не працює. Наслідок прийнято свідомо: доступ до дзеркала рівносильний доступу до всіх паролів у мережі. Маскування лишається там, де конфіг читає людина: план відкату й стенограми завдань. Записано, щоб не переобговорювати. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
017a0689fe
commit
32442f8177
4 changed files with 187 additions and 8 deletions
18
HISTORY.md
18
HISTORY.md
|
|
@ -7844,3 +7844,21 @@ git-дзеркало ДОСЛІВНО. Маскувати їх означає з
|
|||
Заодно з'ясувалось, що ROADMAP брехав: CI не «чекає на раннера» — він
|
||||
працює весь день, задачі 85–111. На коміт c83324a пройшли всі п'ять
|
||||
робіт: hygiene, web, server, dbtest, agent.
|
||||
|
||||
### Рішення власника: дзеркало лишається дослівним (2026-08-28)
|
||||
|
||||
Питання стояло так: тіла конфігів ідуть у git-дзеркало точнісінько
|
||||
такими, як на пристрої, — разом із секретами, які в них є. Маскувати
|
||||
означало б зламати відновлення з архіву, бо цінність NCM саме в
|
||||
побайтовій точності.
|
||||
|
||||
**Обрано точність.** Дзеркало пише конфіг як є; git-сервер вважається
|
||||
довіреним. Пароль скомпрометованого облікового запису власник змінює сам.
|
||||
|
||||
Що це означає надалі, щоб не переобговорювати:
|
||||
|
||||
* маскування діє там, де конфіг читає ЛЮДИНА (план відкату, стенограми
|
||||
завдань) — і саме там сьогодні залатали дірку з відлунням пароля;
|
||||
* архів і дзеркало НЕ маскуються навмисно;
|
||||
* отже, доступ до дзеркала треба вважати рівносильним доступу до всіх
|
||||
паролів у мережі. Це не побічний ефект, а свідомо прийнята ціна.
|
||||
|
|
|
|||
|
|
@ -633,6 +633,14 @@ S3. Приблизно пів дня.
|
|||
(`gofmt`, `go vet`, тести з базою, крос-збірка зонда), проганялось
|
||||
окремо перед кожним комітом.
|
||||
|
||||
### Вирішено й закрито
|
||||
|
||||
**Дзеркало конфігів лишається дослівним** (рішення власника, 2026-08-28).
|
||||
Секрети з конфігів їдуть у git-дзеркало незамаскованими — обрано
|
||||
побайтову точність архіву, бо без неї відновлення з нього не працює.
|
||||
Наслідок прийнято свідомо: доступ до дзеркала рівносильний доступу до
|
||||
всіх паролів у мережі. Маскування лишається там, де конфіг читає людина.
|
||||
|
||||
### Чого не буде без зовнішніх умов
|
||||
|
||||
- **Справжній TLS** на бойовому сервері — потрібен домен. Зараз
|
||||
|
|
|
|||
137
deploy/README.md
137
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)
|
||||
|
||||
**Нова інсталяція вже під політиками — робити нічого не треба.**
|
||||
|
|
|
|||
32
netpulse
32
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue