JOURNAL / 072025-09-16 · Engineering · 9 мин чтения
← все статьи

Postgres RLS: мультитенантность без сюрпризов

Девять месяцев LogiFlow на row-level security: где RLS экономит недели, а где молча отдаёт чужие данные.

Автор Artyom Lebedev · co-founder · backendОбновлено 2025-09-19

Почему не отдельная база на тенанта

Когда у LogiFlow стало 38 перевозчиков на платформе, база-на-тенанта означала бы 38 миграций на каждый релиз и пул соединений, который невозможно держать. Мы положили всех в один Postgres и изолировали через row-level security. Одна роль приложения, отдельные роли для миграций и админки — и никаких per-tenant пользователей в базе, они плодятся и ломают пулинг.

Грабли №1: владелец таблицы обходит RLS

RLS не применяется к суперюзеру и к владельцу таблицы. Если приложение ходит в базу под той же ролью, что владеет таблицами, — политики молча не работают. Мы поймали это на staging, когда один перевозчик увидел рейсы другого. Лечится так: приложение коннектится отдельной не-владельческой ролью, миграции — владельцем, и на каждую таблицу включён FORCE ROW LEVEL SECURITY.

ALTER TABLE loads ENABLE ROW LEVEL SECURITY;
ALTER TABLE loads FORCE  ROW LEVEL SECURITY; -- иначе владелец обойдёт

CREATE POLICY tenant_isolation ON loads
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

-- приложение на каждый запрос:
SET app.tenant_id = '…';   -- НЕ владелец таблицы
screenscomposecomponentscomposeprimitivesvalidatetokenssource
Слои изоляции: роль → политика → индекс

Грабли №2: политика без индекса — это скан

RLS меняет планы запросов: Postgres иначе оценивает число строк. Если на колонке из политики (`tenant_id`) нет индекса — получаете seq scan на каждый запрос. Мы прогнали EXPLAIN ANALYZE с включённым RLS, добавили индексы на колонки политик и заменили подзапросы в политиках на helper-функции. p95 упал с 340мс до 90мс — без единой смены железа.

  • Коннектись не-владельческой ролью, иначе политики молчат
  • FORCE ROW LEVEL SECURITY на каждой таблице
  • Индекс на каждой колонке, которая участвует в политике
  • Helper-функция вместо подзапроса в USING(...)
  • EXPLAIN ANALYZE на каждую политику до прода
«RLS требует тех же тестов, что и код: не проверил политику — она будет молчать ровно тогда, когда должна кричать.»

Что бы сделали иначе

Добавили бы в CI red-team тест с первого дня: запрос, который утверждает, что тенант A физически не может прочитать данные тенанта B. Мы дописали его после случая на staging — а должны были до первой строки кода.

Нужен такой же результат?

Расскажите о проекте — пришлём смету за 24 часа.

Получить смету
⌗ ЖУРНАЛ · ПОДПИСКА

Подписка на журнал

Раз в квартал. Никакого спама. Только новые статьи.

без спама · отписка в один клик