РУКОВОДСТВО ПО БЕЗОПАСНОСТИ СЕРВЕРОВ

Безопасность Supabase: чек-лист RLS, ключей и Storage

Коротко: чтобы защитить приложение на Supabase, включите row-level security (RLS) на каждой таблице в открытой схеме, напишите по одной политике на каждую операцию, не пускайте секретный ключ в браузер, проверьте доступ от имени неавторизованного посетителя, закройте бакеты Storage и запустите Security Advisor до запуска. В документации Supabase сказано: таблица в открытой схеме без RLS доступна на чтение и запись любой роли, у которой есть на неё права.

Row-level security (защита на уровне строк) это функция Postgres, которая привязывает к таблице правила, так что каждый запрос видит и меняет только разрешённые строки. Эта страница описывает часть Supabase из общего чек-листа безопасности вайб-кодинга. Проходите её перед запуском и после каждого изменения схемы.

Быстрый результат (20 минут)

  • Откройте Security Advisor и исправьте все находки «RLS disabled in public». См. шаг 8.
  • Отправьте один запрос на REST-адрес только с publishable-ключом и убедитесь, что приватных строк в ответе нет. См. шаг 4.
  • Найдите во фронтенд-коде и репозитории секретный ключ (он начинается с sb_secret_) или устаревший ключ service_role.
  • Проверьте, что все бакеты Storage с файлами пользователей приватные, а политики ограничивают каждого пользователя его папкой.
  • Убедитесь, что Site URL и список разрешённых redirect-адресов указывают на ваш настоящий домен, а не только на localhost.
Путь запроса в Supabase: вызов REST с ключом, API-шлюз, проверка прав, проверка политик RLS, затем строки отданы или отклонены
Каждый запрос к Data API проходит шлюз, права и RLS; без RLS последняя проверка ничего не делает.

Что открывает Supabase и почему RLS необходим

Каждому проекту Supabase выдаёт REST API (Data API) по адресу вида https://YOUR_PROJECT_REF.supabase.co/rest/v1/. В руководстве по API сказано, что он работает за API-шлюзом с включённой проверкой ключа и согласован с row-level security в Postgres. Ваше приложение обращается к нему напрямую, поэтому между посетителем и базой нет вашего сервера. Единственная проверка это правила в самой базе.

Доступ решается в два слоя, как это описано на странице Securing your API: права (grants) определяют, может ли роль вообще обращаться к объекту, а RLS определяет, к каким строкам. Вы будете встречать роли anon (запрос без вошедшего пользователя) и authenticated (вошедший пользователь).

Там же предупреждение: на существующих проектах новые объекты схемы public автоматически получают права для этих ролей, и поэтому доступны через Data API, даже если вы этого не хотели. Поэтому таблица, созданная ИИ-ассистентом, может быть открыта ещё до первой политики. Документация Lovable использует Supabase как бэкенд приложения и советует перед запуском убедиться, что у каждой таблицы есть политики RLS.

Команды ниже взяты из руководства Hardening the Data API. Общие правила обращения с ключами: управление секретами и переменные окружения.

Шаг 1: включите RLS на каждой открытой таблице

В руководстве по Row Level Security сказано включать RLS на каждой таблице в открытой схеме. Таблица, созданная в Table Editor дашборда, получает его автоматически. Таблица, созданная SQL-запросом, миграцией или другим инструментом, не получает, а ИИ-ассистенты обычно создают таблицы именно через SQL.

alter table public.notes enable row level security;

После включения RLS, по документации, через API с publishable-ключом данные недоступны, пока вы не создадите политики. Не «чините» пустой ответ повторным отключением RLS.

  • Включите RLS на каждой таблице и представлении, до которых дотягивается Data API, включая мелкие справочники.
  • Открывайте меньше. Supabase рекомендует отдельную схему, например api, для открытых объектов, а внутренние таблицы держать в закрытых схемах. Если приложение не использует Data API, его можно отключить.
  • Выдавайте каждой роли только нужные права. Отсутствие права даёт ошибку 42501 (permission denied): выдавайте право осознанно, а не всё подряд.
  • Учтите изменение октября 2026 года. По changelog Supabase, новые таблицы в public перестают открываться автоматически: на существующих проектах это начинается 30 октября 2026 года, а у существующих таблиц права сохраняются. Каждой новой таблице тогда нужны явный grant, RLS и политики в одной миграции. Выполните проверочный запрос из руководства по hardening, потому что значения по умолчанию зависят от возраста проекта.

Чтобы убрать доступ Data API у таблицы, в changelog приведена команда:

revoke all on table public.your_table from anon, authenticated, service_role;

Шаг 2: напишите политики, по одной на операцию

Политика это правило, привязанное к таблице. Важны две части: USING определяет, какие существующие строки операция может видеть или затрагивать (для select, delete и старой строки при update), а WITH CHECK определяет, допустима ли новая или изменённая строка (для insert и новой строки при update). В примере таблица public.notes, где каждая строка принадлежит пользователю:

create table public.notes (
  id bigint generated always as identity primary key,
  user_id uuid not null references auth.users (id),
  body text not null
);

alter table public.notes enable row level security;

grant select, insert, update, delete on table public.notes to authenticated;

create policy "Users read own notes" on public.notes
  for select to authenticated
  using ( (select auth.uid()) = user_id );

create policy "Users add own notes" on public.notes
  for insert to authenticated
  with check ( (select auth.uid()) = user_id );

create policy "Users edit own notes" on public.notes
  for update to authenticated
  using ( (select auth.uid()) = user_id )
  with check ( (select auth.uid()) = user_id );

create policy "Users delete own notes" on public.notes
  for delete to authenticated
  using ( (select auth.uid()) = user_id );

Строка с grant нужна только на проектах, где новые таблицы не получают права автоматически (см. шаг 1), и она намеренно не затрагивает anon. Функция auth.uid() возвращает ID вошедшего пользователя. У политики на обновление есть обе части, чтобы пользователь не мог передать строку другому.

  • Одна политика на операцию. Документация не советует for all, потому что оно скрывает, какую операцию покрывает правило. Для update нужна и подходящая политика select.
  • Указывайте роль через to. Политика вроде to anon using ( true ) открывает все строки, до которых роль дотягивается по правам. Используйте её только для данных, которые должны быть публичными.
  • У неавторизованного посетителя auth.uid() равен null, и сравнение ничего не находит. Анонимные входы в Supabase Auth получают роль authenticated, так что «authenticated» не всегда означает зарегистрированного человека.
  • Никогда не основывайте доступ на user_metadata, потому что пользователи могут его менять. Для данных авторизации документация указывает app_metadata.
  • Производительность: документация советует (select auth.uid()) вместо auth.uid(), чтобы Postgres мог кешировать результат, и индекс на столбцы, по которым фильтруют политики.

Основы описаны в статье глоссария row-level security и в принципе наименьших привилегий.

Шаг 3: разберитесь с ключами: publishable и secret

В руководстве по ключам API описаны два вида ключей для Data API. От того, какой из них попадёт во фронтенд, зависит цена утечки.

  • Publishable-ключ (начинается с sb_publishable_): по документации, его безопасно открывать на веб-странице, в мобильном приложении, CLI и исходном коде. Без вошедшего пользователя он соответствует роли anon, а возможности ограничивает RLS. Только этот ключ должен быть в браузерном коде.
  • Secret-ключ (начинается с sb_secret_): только для серверных компонентов, например серверов, Edge Functions и микросервисов. Он соответствует роли service_role и, по документации, обходит все политики RLS. Никогда не кладите его в браузер, распространяемое приложение или систему контроля версий, даже на localhost.
  • Устаревшие ключи: прежний ключ anon это устаревшая версия publishable-ключа, а service_role устаревшая версия secret-ключа. Обе системы работают параллельно; по документации, устаревшие ключи выводятся из обращения до конца 2026 года, и рекомендуется перейти на новые.
  • Храните secret-ключ как серверный секрет. Не держите его в системе контроля версий и скриптах CI, используйте хранилище секретов вашего инструмента и работайте с ним только на машинах, которые вы контролируете. Подробности: открытые .env и .git и безопасный деплой приложения на VPS.
  • Не полагайтесь на блокировку в браузере. Supabase отвечает на secret-ключ из браузера кодом HTTP 401, но, как сказано в документации, злоумышленник всё равно может использовать утёкший ключ из других инструментов.

Если secret-ключ утёк, сначала устраните причину утечки, затем меняйте ключ. Порядок по документации: создайте новый secret-ключ, замените старый везде, убедитесь, что каждый компонент использует новый, и только потом удалите старый, потому что удаление необратимо. Устаревший ключ сначала деактивируйте: это обратимо, если вы забыли какой-то клиент. Полный порядок действий: что делать, если ключ API утёк.

Сравнение ключей Supabase: secret-ключ остаётся на сервере, publishable-ключ можно использовать в браузере при включённом RLS
Secret-ключи обходят row-level security, поэтому их место только в серверном коде и серверных секретах.

Шаг 4: проверьте доступ как посетитель и как другой пользователь

Не доверяйте политике, которую вы не пытались сломать. Проверяйте снаружи, имея только публичный ключ. В quickstart Supabase показан формат запроса: имя таблицы после REST-адреса, ключ в заголовке apikey.

curl 'https://YOUR_PROJECT_REF.supabase.co/rest/v1/notes?select=*' -H "apikey: YOUR_PUBLISHABLE_KEY"
Терминал с REST-запросом только с publishable-ключом: пустой список безопасен, а вернувшиеся строки значат, что RLS не хватает
Иллюстративный вывод: пустой список или ошибка прав это безопасный результат для приватной таблицы.

Токен пользователя не передаётся, так что это неавторизованный посетитель. Подставьте идентификатор проекта и publishable-ключ. Безопасный результат:

  • Безопасно: пустой список [] для приватных таблиц или ошибка прав, например 42501. И то и другое значит, что посторонний ничего не получает.
  • Небезопасно: любые JSON-строки из таблицы с данными пользователей. Включите RLS, добавьте политики и повторите запрос.

Затем проверьте доступ от имени конкретного пользователя. Проще всего тест двух пользователей из чек-листа вайб-кодинга: войдите в приложение как пользователь B и попробуйте открыть заметку пользователя A по её ID. Запрос можно воспроизвести и в SQL. В документации идентичность подменяют через set role и JWT claims, а транзакция ниже всё откатывает в конце (подставьте два настоящих ID пользователей вашего проекта):

begin;
set local role authenticated;
set local request.jwt.claims to '{"role":"authenticated","sub":"USER_A_UUID"}';

select * from public.notes;   -- ожидаем только строки пользователя A
insert into public.notes (user_id, body)
  values ('USER_B_UUID', 'test');   -- ожидаем ошибку row-level security

rollback;

Для повторяемых проверок в документации описаны тесты pgTAP, которые запускаются командой supabase test db.

Ловушка SQL-редактора. Не проверяйте политику обычным select в SQL-редакторе. В документации объяснено: владелец таблицы и роли с BYPASSRLS политикам не подчиняются, поэтому тот же запрос в SQL-редакторе возвращает строки, а API ничего не возвращает. Запрос, который работает там, ничего не доказывает про то, что видят ваши пользователи. Сначала переключите роль, как выше, или проверяйте через API.

Нужен второй взгляд на ваш стек?

Бесплатная проверка в режиме только чтения покажет, что открыто на сервере, где работает приложение, и что исправлять в первую очередь.

Получить бесплатную проверку сервера Связаться с инженером по безопасности

Шаг 5: проверьте представления и функции, обходящие RLS

RLS защищает таблицы. Обойти его можно двумя способами.

  • Представления (views). По документации, представления по умолчанию обходят RLS, потому что их обычно создаёт пользователь postgres. В Postgres 15 и новее создавайте представление с security_invoker = true, и оно будет подчиняться политикам исходных таблиц. В более старых версиях отзовите у anon и authenticated доступ к представлению или держите его в закрытой схеме.
    create view public.my_notes
      with (security_invoker = true) as
      select id, body from public.notes;
  • Функции. В документации сказано, что RLS к функциям не применяется, поэтому выдавайте execute только тем ролям, которым это нужно. Функцию security definer (она выполняется с правами владельца) проверяйте особенно внимательно, фиксируйте её search_path, указывайте схему в именах и никогда не создавайте её в схеме, открытой в настройках API.

Шаг 6: закройте бакеты Storage

Supabase Storage хранит файлы в бакетах, а доступ к файлам задаётся политиками RLS на таблице storage.objects, как объясняет руководство по контролю доступа в Storage. По умолчанию загрузка в бакеты без политик не разрешена, а в основах бакетов сказано, что бакеты приватны по умолчанию.

  • Читайте «публичный бакет» как «любой, у кого есть ссылка, может читать». По документации, публичный бакет фактически обходит контроль доступа при получении и выдаче файлов, и любой, у кого есть URL файла, может его прочитать. Загрузка, удаление, перемещение и копирование по-прежнему проверяются. Счета, документы и загрузки пользователей держите в приватных бакетах.
  • Пишите политики по операциям. Для загрузки нужна политика insert. Для замены файла (upsert) дополнительно нужны select и update.
  • Ограничьте пользователя его папкой. В документации для проверки первой папки пути используется storage.foldername(name):
    create policy "Users upload to own folder" on storage.objects
      for insert to authenticated
      with check (
        bucket_id = 'user-files'
        and (storage.foldername(name))[1] = (select auth.uid()::text)
      );
    Для чтения файлов нужна подходящая политика select с тем же условием.
  • Избегайте широких политик select на публичных бакетах. В Security Advisor есть проверка «public bucket allows listing»: клиенты тогда могут перечислить все файлы бакета.

Шаг 7: усильте Auth

RLS работает, только если можно доверять тому, кто этот пользователь. В Supabase Auth есть несколько настроек, которые стоит проверить до запуска. Они взяты из чек-листа выхода в продакшен и руководств по Auth.

  • Требуйте подтверждение email. В руководстве по email-аутентификации сказано, что на размещённых проектах оно включено по умолчанию, а при локальной разработке и на self-hosted выключено, поэтому проверьте настройку там, где реально работаете.
  • Используйте собственный SMTP-сервер в продакшене. Встроенная почтовая служба ограничена 2 письмами в час и работает по принципу best-effort.
  • Укажите Site URL и список разрешённых redirect-адресов на настоящий домен. Руководство по redirect URL называет Site URL критически важным для писем подтверждения и сброса пароля и советует в продакшене указывать точный путь. Шаблоны вроде ** оставьте для локальной разработки и preview-хостов.
  • Предложите многофакторную аутентификацию там, где данные чувствительны. Supabase Auth поддерживает MFA через приложение-аутентификатор (TOTP) и SMS, а требовать её можно в политике: (select auth.jwt()->>'aal') = 'aal2' в restrictive-политике. Включите её и для собственного аккаунта Supabase. См. двухфакторная аутентификация.
  • Ограничьте одноразовые коды и злоупотребления. Чек-лист рекомендует срок действия OTP не более 3600 секунд, удаление тестовых номеров и CAPTCHA для регистрации, входа и сброса пароля. Добавьте ограничение частоты запросов на собственных эндпоинтах.

Шаг 8: пусть Security Advisor найдёт пропущенное

Советники базы данных (advisors) это детерминированные проверки проекта. Их запускают со страницы Security Advisor в дашборде, командой CLI supabase db advisors, а также через Management API и MCP. Находки бывают намеренными, поэтому перед правкой сверяйте каждую с задуманной моделью доступа. На что смотреть:

  • 0013 RLS disabled in public (ошибка): по документации, любой, кто знает URL проекта, может читать, менять и удалять все данные таблицы. Включите RLS.
  • 0010 Security definer view (ошибка): представление игнорирует RLS. Пересоздайте его с security_invoker=on.
  • 0002 Auth users exposed (ошибка): представление раскрывает персональные данные пользователей. Замените его таблицей профилей.
  • 0008 RLS enabled, no policy (информация): через API нельзя ни читать, ни писать. Добавьте политику.
  • 0011 Function search path mutable (предупреждение): задайте функции фиксированный search path.
  • 0025 Public bucket allows listing (предупреждение): удалите широкую политику select или сделайте бакет приватным.

Советник не знает ваших бизнес-правил: политика using ( true ) всё равно открывает всё, поэтому шаг 4 обязателен. Если в приложении данные клиентов и безопасностью никто не занимается, попросите второе мнение; см. управляемая безопасность или DIY.

Предзапусковая проверка на одном экране

Пройдите этот список до первого настоящего пользователя и после каждого изменения схемы или Auth. Аналогичный материал для бэкенда Google: чек-лист правил безопасности Firebase, а правила для ИИ-ассистентов, которые пишут ваши миграции, в статье правила безопасности для ИИ-ассистентов программиста.

  • RLS включён на каждой таблице и представлении, до которых дотягивается Data API.
  • По политике на операцию, роли указаны через to, нет using ( true ) на приватных данных; права (grants) явные и минимальные.
  • Во фронтенде только publishable-ключ; secret-ключ лежит в серверном секрете и никогда не коммитился.
  • Запрос только с publishable-ключом не возвращает ничего приватного; тест двух пользователей пройден.
  • Представления используют security_invoker; функции security definer проверены и не лежат в открытых схемах.
  • Бакеты с файлами пользователей приватные, с политиками по папкам.
  • Подтверждение email включено, Site URL и список redirect верны, MFA предложена там, где данные чувствительны.
  • В Security Advisor нет ошибок, которые вы не можете объяснить.
  • Бэкапы базы есть, и восстановление пробовали. См. стратегию бэкапов 3-2-1.

Ещё для вайб-кодеров: все руководства серии собраны в центре безопасности для вайб-кодеров, а пункты можно отмечать в интерактивном чек-листе.

Часто задаваемые вопросы

Что такое row level security в Supabase?

Row-level security (RLS) это функция Postgres, которая привязывает к таблице правила, так что каждый запрос читает и меняет только разрешённые строки. Без RLS таблица в открытой схеме доступна на чтение и запись любой роли, у которой есть на неё права.

Безопасно ли открывать ключ anon в Supabase?

Supabase описывает publishable-ключ и прежний ключ anon как безопасные для веб-страницы и приложения, потому что возможности ограничивает RLS. Это верно только если на каждой открытой таблице включён RLS и есть корректные политики. Secret-ключ и прежний service_role открывать нельзя никогда.

Что делать, если утёк ключ service role в Supabase?

Устраните причину утечки, создайте новый secret-ключ, замените старый везде, убедитесь, что каждый компонент использует новый, и затем удалите старый. Устаревший ключ сначала деактивируйте: это обратимо, если вы забыли какой-то клиент.

Как проверить, что таблицы Supabase не читаются публично?

Отправьте запрос на REST-адрес таблицы только с publishable-ключом и посмотрите, вернутся ли строки. Затем откройте Security Advisor и найдите находку «RLS disabled in public». Пустой список или ошибка прав значит, что посторонний ничего не получает.

Почему запрос работает в SQL-редакторе, но в приложении ничего не возвращает?

SQL-редактор работает с правами, которые обходят политики: владелец таблицы и роли с BYPASSRLS им не подчиняются. Ваше приложение работает под ролью anon или authenticated и видит только то, что разрешают политики. Добавьте политику и проверяйте, переключая роль в SQL или вызывая API.

Нужен ли RLS, если приложение работает с Supabase только через мой сервер?

Если сервер подключается к Postgres напрямую и Data API не используется, Supabase советует отключить Data API. Если Data API остаётся включённым, всё равно включите RLS: всё, что доступно через него, защищено только правами и политиками.

Безопасны ли публичные бакеты Storage?

Только для файлов, предназначенных для всех. По документации Supabase, публичный бакет обходит контроль доступа при чтении, поэтому любой, у кого есть URL файла, может его скачать, а загрузка и удаление по-прежнему проверяются политиками. Документы пользователей держите в приватных бакетах с политиками по папкам.

Не уверены, в каком состоянии ваш сервер?

Получите бесплатную проверку безопасности в режиме только чтения или поговорите с инженером по безопасности об управляемой защите.

Получить бесплатную проверку сервера Связаться с инженером по безопасности