# Увімкнення RLS на інсталяції, старшій за 0063 > **Цей документ потрібен ЛИШЕ інсталяціям, зробленим до міграції 0063.** > На системі, розгорнутій із цією версією, RLS увімкнено з першого > запуску: `netpulse-migrate` бачить порожню базу, накочує схему й > одразу видає ролям паролі з `.env`. Робити не треба нічого, читати це > теж не треба. > > Як перевірити, що це саме ваш випадок: > > ```sh > docker compose exec -T db psql -U netpulse -d netpulse -c \ > "SELECT fresh, applied_was, decided_at FROM public.netpulse_install" > ``` > > `fresh = t` — інсталяція народилась із RLS, далі не читайте. `fresh = f` > — база вже працювала, коли її вперше побачив мігратор із підтримкою > ролей; перехід на ній робиться руками, і саме про це документ нижче. > Таблиці немає взагалі — стенд ще не оновлювався; оновіть образи й > накотіть міграції (крок 1), рядок з'явиться. Як увімкнути справжню ізоляцію кабінетів на живому стенді, що перевіряти після кожного кроку і як відкотитись, якщо застосунок перестане бачити дані. ## Чому це не робиться саме Мігратор уміє видавати ролям паролі й робить це на кожній новій інсталяції. На вашій він цього навмисно не робить, і ознака, за якою він розрізняє два випадки, — не здогад, а записаний факт: чи була `public.schema_migrations` порожня в ту мить, коли мігратор уперше побачив цю базу. Відповідь пишеться один раз у `public.netpulse_install` і більше не переглядається. Причина проста. На порожній базі перемикати нічого: немає ані даних, ані клієнтів, ані стану, у який можна повернутись. На вашій — є все три. Перемикання роллю на живому стенді має вікно, у якому зонди можуть замовкнути, і мусить мати крок, на якому можна зупинитись. Тому воно лишається процедурою, а `docker compose up` його не запускає. ## Що саме змінюється Політики Row Level Security написані в схемі з міграції 0011 і стоять на 68 таблицях. Жодна з них ніколи не спрацьовувала: `docker-compose.yml` збирає DSN із ролі `netpulse`, а її створює образ Postgres зі змінної `POSTGRES_USER`, тобто bootstrap-суперкористувачем. Суперкористувач обходить RLS беззастережно. Тобто ізоляцію кабінетів у продукті тримає рівно одне: те, що кожен запит у `server/internal/store` дописує `tenant_id = $1` руками. Один забутий предикат — і клієнт бачить чужі хости. Другий рубіж написаний, увімкнений у схемі й вимкнений у житті. Перехід дає три ролі: | роль | BYPASSRLS | хто ходить | |---|---|---| | `netpulse` | так (суперкористувач) | `migrate`, `netpulse-user`, `netpulse-secret` | | `netpulse_app` | **ні** | `api`, `collector` | | `netpulse_worker` | так | фонові такти всередині `api` і `collector`, `netpulse-gitsync` | `netpulse_worker` лишається з BYPASSRLS свідомо: запити-шукачі черг (`ClaimConfigJobs`, `ClaimCommandJobs`, `DuePolicies`, `FetchEvents`) — це одна інструкція `UPDATE … FOR UPDATE SKIP LOCKED … RETURNING tenant_id` на всю інсталяцію. Розкласти її по кабінетах означає замінити один такт на N тактів кожні 5 секунд і завести голодування. Обґрунтування цілком — у коментарі до `0063_rls_enforce.sql`, розділ 1. ## Чого цей перехід не робить - **Не змінює телеметрію.** Гіпертаблиці (`ts.*`, `core.audit_log`, `alr.alerts_history`, `alr.notifications`) під RLS не підпадають — і не можуть, поки ввімкнено стиснення. Їхню ізоляцію й далі тримає предикат у запиті. Це не наслідок переходу, а незмінна властивість TimescaleDB. - **Не рятує від BYPASSRLS у воркера.** Роль воркера бачить усе. Її обмежує не RLS, а те, ким і звідки вона використовується: окремий пул у `store.Store.bg`, окремий пароль, скінченний перелік методів (`grep -rn 's\.bg\.' server/internal/store/`). - **Не переносить володіння об'єктами.** `netpulse` лишається власником усіх 100+ таблиць. Передавати ownership на живій базі — це `ALTER TABLE … OWNER TO` на кожну гіпертаблицю з чанками, тобто довга блокувальна дія на чужих даних заради нуля користі. ## Передумови - Свіжий дамп бази (`## Бекап` у `deploy/README.md`) — знятий **сьогодні**, не «десь був». - Вікно, у якому допустимо, що зонди на кілька хвилин перестануть доповідати. Дані за цей час не губляться: агент тримає їх у себе й дошле, — але алерти в цей проміжок не рахуються. - Доступ до `docker compose exec db psql`. Далі всюди мається на увазі, що ви в каталозі з `docker-compose.yml`. --- ## Крок 1. Накотити 0063 ```sh git pull docker compose build docker compose run --rm migrate ``` **Це безпечно й нічого не вмикає.** 0063 заводить ролі без пароля (підключитись ними ще не можна), роздає права, закриває політиками шість зв'язкових таблиць і ставить `security_invoker` на два вигляди. Поки застосунок ходить суперкористувачем, жодна з цих змін на нього не діє. Міграція сама себе перевіряє: якщо в схемі є таблиця з `tenant_id` без RLS або без політики, або таблиця, до якої `netpulse_app` не має SELECT, вона впаде з переліком таких таблиць. Падіння тут означає «переходити ще рано», а не «щось зламалось». На цьому ж запуску мігратор запише `public.netpulse_install` — рядок, який назавжди фіксує, що ця база НЕ була порожньою, коли він її вперше побачив. Саме через нього все подальше лишається ручним: паролі ролям на такій базі він не видасть ні зараз, ні через рік, скільки б рядків не з'явилось у `.env`. **Перевірити:** ```sh docker compose exec -T db psql -U netpulse -d netpulse -c \ "SELECT rolname, rolcanlogin, rolbypassrls FROM pg_roles WHERE rolname LIKE 'netpulse%' ORDER BY 1" ``` Очікується рівно це: ``` rolname | rolcanlogin | rolbypassrls -----------------+-------------+-------------- netpulse | t | t netpulse_app | t | f netpulse_worker | t | t ``` `rolbypassrls = f` у `netpulse_app` — головний рядок цієї таблиці. Якщо там `t`, далі йти немає сенсу: усе наступне пройде, і не змінить нічого. Застосунок на цьому кроці не чіпаємо. Можна зупинитись тут на добу. --- ## Крок 2. Видати паролі Паролі не лежать у міграції навмисно: у git і в контрольній сумі `public.schema_migrations` вони були б назавжди. **Тільки hex.** Пароль ролі їде всередині DSN `postgres://роль:пароль@db:5432/netpulse`, і не кожен символ лишається там собою. `openssl rand -base64` рано чи пізно видасть `/` — скісна риска обриває пароль і перетворює його хвіст на ім'я бази: з'єднання не встановлюється, у журналі стоїть «database … does not exist». Гірший випадок — `%`: розбирач читає його як початок `%XX`, помилки немає, а пароль мовчки стає іншим рядком. Виглядає це не як зіпсований рядок у `.env`, а як «RLS усе зламав», і півдня цього проєкту коштувало саме воно. ```sh APP_PW=$(openssl rand -hex 24) WRK_PW=$(openssl rand -hex 24) docker compose exec -T db psql -U netpulse -d netpulse < 'ПЕРШИЙ-КАБІНЕТ-UUID'; SELECT 'зв''язки: доступи' AS q, count(*) FROM inv.device_credentials; SELECT 'вигляд лінків' AS q, count(*) FROM topo.link_live; SET LOCAL app.tenant_id = ''; SELECT 'без контексту' AS q, count(*) FROM inv.devices; RESET ROLE; ROLLBACK; SQL ``` Очікується: «бачить своїх» — реальна кількість хостів кабінету, «бачить чужих» — **0**, «без контексту» — **0**. Кількість у зв'язках і у вигляді `topo.link_live` має відповідати цьому ж кабінету, а не всій інсталяції. Якщо «бачить чужих» більше нуля — зупиніться. Далі йти не можна: це означає, що якась таблиця лишилась без політики, і перехід дасть хибне відчуття захисту замість захисту. Той самий сценарій у вигляді тесту, який ганяється на одноразовій базі: `server/internal/store/rls_isolation_test.go`. --- ## Крок 4. Перемкнути застосунок У `.env` дописати два рядки (пароль ролі-власника лишається на місці — ним ходять міграції й утиліти): ```sh NETPULSE_APP_PASSWORD= NETPULSE_WORKER_PASSWORD= ``` Обидва — разом. `NETPULSE_APP_PASSWORD` без `NETPULSE_WORKER_PASSWORD` дає найгірший з можливих станів: інтерфейс працює, а фонові такти мовчки нічого не знаходять — бекапи не запускаються, алерти не розсилаються, події не доходять до браузера, і жодної помилки в журналі при цьому немає. Тепер цю пару перевіряє мігратор і зупиняє запуск, але покладатись на це не варто: він рятує від забутого рядка, а не від неправильного пароля. Окремої змінної `NETPULSE_APP_USER` більше немає. Раніше вона була, і будь-яка з двох половин без другої давала DSN, який не встановлюється: нова роль зі старим паролем або стара роль з новим. Тепер ім'я ролі випливає з наявності пароля. Якщо `NETPULSE_APP_USER` лишився у вашому `.env` — його просто ігнорують, видаляти не обов'язково. ```sh docker compose up -d api collector ``` `api` і `collector` залежать від `migrate`, тож перед ними ще раз відпрацює мігратор. Схему він не змінить (вона актуальна), паролів ролям не видасть (база непорожня), але зайде обома DSN і перевірить, що `netpulse_app` заходить і не має BYPASSRLS, а `netpulse_worker` заходить і має. Якщо крок 2 пропущено, ви побачите це тут, а не за годину в журналі колектора. **Перевірити протягом перших п'яти хвилин:** 1. **Інтерфейс.** Увійти й відкрити перелік хостів. Порожній перелік у непорожньому кабінеті — ознака, що щось лишилось без політики. ```sh curl -sf https://$NETPULSE_DOMAIN/healthz ``` 2. **Зонди.** Це ламається першим, якщо ламається: ```sh docker compose exec -T db psql -U netpulse -d netpulse -c \ "SELECT status, count(*), max(last_heartbeat_at) FROM core.agents GROUP BY 1" ``` `max(last_heartbeat_at)` має бути свіжішим за хвилину. Якщо він застиг на моменті перезапуску — агенти не автентифікуються, і це видно ще й у журналі колектора: ```sh docker compose logs --since 5m collector | grep -i unauth ``` 3. **Телеметрія доходить:** ```sh docker compose exec -T db psql -U netpulse -d netpulse -c \ "SELECT max(ts) FROM ts.icmp_samples" ``` 4. **Фонові такти живі.** Черга завдань не має рости монотонно: ```sh docker compose exec -T db psql -U netpulse -d netpulse -c \ "SELECT status, count(*) FROM ncm.jobs GROUP BY 1" ``` Повторити через п'ять хвилин. Якщо `queued` росте, а `running` і `done` стоять — воркер не бачить черги, тобто `NETPULSE_WORKER_PASSWORD` не доїхало. 5. **Події доходять у браузер.** Відкрита сторінка має оновлювати статуси без перезавантаження. Непрямо: ```sh docker compose exec -T db psql -U netpulse -d netpulse -c \ "SELECT count(*) FROM core.event_outbox WHERE published_at IS NULL" ``` Число має коливатись, а не тільки зростати. **Перевірити протягом першої доби:** 6. **Бекап конфігів відпрацював за розкладом:** ```sh docker compose exec -T db psql -U netpulse -d netpulse -c \ "SELECT max(created_at) FROM ncm.configs" ``` 7. **Алерти рахуються.** Погасити тестовий хост і переконатись, що алерт з'явився й прийшов у канал. 8. **Журнал аудиту показує імена акторів, а не порожні клітинки.** Це єдине місце, де помилка виглядає правдоподібно: перелік подій лишається, а колонка «хто» стає порожньою. --- ## Крок 5. Утиліти командного рядка `netpulse-user` (заводить кабінети й людей) і `netpulse-secret` (кладе паролі в `core.secrets`) роблять рівно те, чого роль під RLS робити не має. Після переходу їм потрібен DSN власника — для цього в `docker-compose.yml` є окрема служба `cli`: ```sh docker compose run --rm --entrypoint netpulse-user cli \ -tenant default -login admin -role owner ``` Без неї вони не впадуть з помилкою, а мовчки нічого не знайдуть. `netpulse-gitsync`, навпаки, ходить `NETPULSE_DSN_WORKER` і працює як є — перелік кабінетів він бере крос-тенантним запитом. --- ## Відкат Відкат — не міграція. 0063 нічого не ламає й лишається накоченим; назад повертається тільки те, якою роллю ходить застосунок. **Швидкий (30 секунд, без втрати даних):** ```sh # У .env закоментувати або прибрати два рядки: # NETPULSE_APP_PASSWORD, NETPULSE_WORKER_PASSWORD docker compose up -d api collector ``` DSN згортається до `netpulse` — тобто до стану «до переходу», разом із BYPASSRLS. Перевірка та сама, що на кроці 4, пункти 1–4. Прибирати треба обидва рядки, і саме тому їх лишилось два, а не три: одна змінна вирішує і роль, і пароль, тому «прибрав половину» більше не є станом, у який можна потрапити. Це працює, бо порожній `NETPULSE_APP_PASSWORD` збирає старий DSN, а порожній `NETPULSE_WORKER_PASSWORD` лишає `NETPULSE_DSN_WORKER` порожнім — і `store.UseWorkerDSN` тоді просто не відкриває другий пул, а фонові запити йдуть основним. Жодного коду вимикати не треба. **Якщо відкат не допоміг** — значить справа не в ролях, і схема тут ні до чого: 0063 не змінює жодної таблиці з даними. Дивіться, що ще поїхало разом із цим релізом. **Схему назад не котять.** Зворотних міграцій у проєкті немає навмисно (`deploy/README.md`, `## Оновлення`), і 0063 тут не виняток. Якщо треба прибрати саме її наслідки — це три команди, і жодна не чіпає даних: ```sql ALTER ROLE netpulse_app NOLOGIN; ALTER ROLE netpulse_worker NOLOGIN; ALTER VIEW topo.link_live SET (security_invoker = false); ``` Політики на зв'язкових таблицях лишати можна: під суперкористувачем вони не діють. --- ## Що зробити потім 1. **Звузити права `netpulse_worker`.** Зараз він має `SELECT, INSERT, UPDATE, DELETE` на все — успадковано з 0011. Звужувати наосліп, за читанням коду, — спосіб зупинити бекапи через півтори доби на таблиці, про яку забули. Правильний порядок: дати стенду відпрацювати тиждень, зняти фактичний перелік і звузити за ним. ```sql -- увімкнути на добу, потім зняти перелік ALTER SYSTEM SET pg_stat_statements.track = 'all'; SELECT calls, query FROM pg_stat_statements WHERE userid = 'netpulse_worker'::regrole ORDER BY calls DESC; ``` 2. **Прибрати другий рубіж там, де він більше не потрібен?** Ні. Явний `tenant_id = $1` у запитах лишається: на гіпертаблицях він єдиний, а на решті — те, що робить план запиту передбачуваним (політика додає умову, індекс використовує предикат). 3. **Стежити за новими таблицями.** Перевірка в 0063 разова — вона спрацювала на момент накочування. Наступна таблиця з `tenant_id` без політики знову з'явиться мовчки. Найдешевше — повторити ту саму перевірку в наступній міграції, що додає таблиці.