Supabase "RLS disabled in public" 경고 — 켜기 전에 확인할 것

바이브코딩119 · 2026-07-22 · 데이터·보안
최종 확인 2026-07-23적용 Supabase공식 출처 6개

Security Advisor에 빨간 경고가 뜨면 Enable RLS부터 누르기 쉽습니다. 운영 중인 앱에서는 그 한 번으로 조회와 저장이 함께 막힐 수 있습니다. 반대로 화면이 깨질까 봐 미루면 노출은 계속됩니다. 테이블의 쓰임과 현재 권한·기존 정책을 확인한 뒤, 필요한 변경을 한 묶음으로 적용하세요.

스위치를 누르기 전에 네 가지를 봅니다

  1. 이 테이블이 Data API에 노출되어 있나요?
  2. anonauthenticated 역할에 어떤 GRANT가 있나요?
  3. 이미 만들어진 정책 가운데 using (true)처럼 넓게 여는 것이 있나요?
  4. 브라우저가 publishable key가 아닌 secret·service-role key를 쓰고 있지는 않나요?

RLS disabled in publicpublic 스키마의 테이블에 행 단위 보안이 꺼져 있다는 경고입니다. GRANT는 역할이 테이블에서 어떤 동작을 할 수 있는지 정하고, RLS는 그 동작이 허용된 을 좁힙니다. 둘 중 하나가 다른 하나를 대신하지 않습니다.

RLS는 열을 숨기지 않습니다. 같은 행에 이메일, 내부 메모처럼 브라우저에 보내면 안 되는 열이 있다면 전체 테이블 SELECT 권한을 주지 말고 열 단위 권한, 필요한 열만 담은 PostgreSQL 15 이상의 security_invoker 뷰, 또는 서버 API를 따로 설계해야 합니다.

Supabase의 2026년 변경도 프로젝트 생성일만 보고 단정하면 안 됩니다. 5월 30일부터 새 프로젝트에 새 기본값이 순차 적용됐고, 10월 30일부터 기존 프로젝트에도 적용될 예정입니다. 이 변경은 앞으로 만드는 테이블의 기본 GRANT에 관한 것이며 기존 테이블의 권한을 자동 회수하지 않습니다. 현재 프로젝트의 설정과 실제 권한을 직접 확인하세요.

1. 권한과 기존 정책을 먼저 펼쳐 봅니다

Database → Security Advisor에서 대상 테이블을 확인한 뒤, 로그인 전 조회가 필요한지, 로그인한 사용자만 써야 하는지, 서버에서만 다룰지 정합니다. SQL Editor에서는 현재 정책을 이렇게 볼 수 있습니다.


select policyname, permissive, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'public' and tablename = 'profiles';

정책은 새 정책을 추가한다고 자동으로 더 엄격해지지 않습니다. 기본인 permissive 정책 여러 개는 OR로 합쳐지므로, 예전에 만든 using (true)to public 정책 하나가 사용자별 정책보다 넓게 열 수 있습니다. 새 정책을 붙이기 전에 불필요한 기존 정책을 삭제하거나 범위를 고치세요. 같은 이름의 정책이 이미 있다면 CREATE POLICY를 반복하지 말고 정의를 확인한 뒤 ALTER POLICY나 검토된 DROP POLICY 마이그레이션을 사용합니다.

2. RLS·권한·정책을 한 번에 준비합니다

아래 예시는 profiles.user_id에 로그인 사용자 ID를 저장하고, 로그인 사용자에게 자기 행의 조회·추가·수정·삭제를 허용하는 구조입니다. 테이블명, 열 이름, 실제 제품 규칙에 맞춰 바꾼 뒤 스테이징에서 먼저 실행하세요.


begin;

alter table public.profiles enable row level security;

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

create policy "Users can view own rows"
on public.profiles for select
to authenticated
using ( (select auth.uid()) = user_id );

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

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

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

commit;

using은 UPDATE 전의 행을 수정할 수 있는지 보고, with check는 수정 결과도 소유 조건을 지키는지 봅니다. with check를 생략하면 PostgreSQL이 using 조건을 재사용하지만, 의도를 검토하기 쉽도록 둘 다 쓰는 편이 낫습니다. UPDATE가 정상 동작하려면 해당 행을 볼 수 있는 SELECT 정책도 필요합니다.

서버에서만 쓰는 테이블이라면 억지로 클라이언트 정책을 만들지 말고 revoke all on table public.profiles from anon, authenticated;처럼 Data API 역할 권한을 회수합니다. 반대로 앱에서 필요한 테이블은 위 예시처럼 동작별 GRANT를 명시하세요. INSERT가 시퀀스를 직접 사용하는 구조라면 그 시퀀스에도 필요한 최소 권한을 별도로 줍니다. 로그인 전 공개 데이터는 anon용 정책을 따로 만들되, 공개할 행과 열을 나눠 검토합니다.

Table Editor로 만든 테이블은 RLS가 기본 활성화되지만 raw SQL이나 SQL Editor로 만든 테이블은 직접 켜야 합니다. RLS를 켠 뒤 화면이 비었다면 데이터가 지워진 것이 아니라, 적용되는 정책이나 GRANT가 없어 요청이 막힌 경우가 많습니다.

3. 키는 모든 사용처를 옮긴 뒤 예전 것을 끕니다

브라우저에는 publishable key(sb_publishable_...)를 사용합니다. 기존 프로젝트의 anon 키도 공개용이지만 새 키 체계로 옮기는 편이 좋습니다. secret key(sb_secret_...)와 기존 service_role은 RLS를 우회하므로 서버 밖에 두면 안 됩니다.

새 키를 만드는 것만으로 기존 키가 폐기되지는 않습니다. 다음 순서로 옮깁니다.

  1. 웹·모바일·데스크톱·배포된 클라이언트의 anon을 publishable key로 바꿉니다.
  2. 서버, Edge Functions, 워커, CI/CD, 크론, 외부 연동, Database Webhooks와 pg_netservice_role을 secret key로 바꿉니다.
  3. 새 secret key는 Authorization: Bearer가 아니라 apikey 헤더로 보내야 하는 연동이 있는지 확인합니다.
  4. 이전 키를 찾는 요청이 더 없는지 확인한 뒤 Settings → API Keys에서 legacy anon·service_role 키를 비활성화합니다.

노출된 sb_secret_...는 해당 키를 새로 만들어 서버를 교체한 뒤 이전 키를 폐기합니다. legacy service_role이 노출된 경우에도 일부 코드만 바꾸고 끝내지 말고 위 전체 이전을 마친 뒤 legacy 키를 비활성화하세요. 앱에 배포된 구버전, 자동 작업, 웹훅이 남아 있으면 전환 때 끊길 수 있으므로 목록을 만들어 확인합니다.

4. 실제 역할과 두 사용자로 확인합니다

SQL Editor에는 사용자 JWT가 없어서 select auth.uid();null인 것이 정상입니다. SET LOCAL은 현재 트랜잭션에만 적용되므로 반드시 BEGINROLLBACK 사이에서 사용하세요.


begin;
set local role authenticated;
set local request.jwt.claim.sub = '<테스트할 사용자의 UUID>';
select * from public.profiles;
rollback;

이 SQL은 빠른 확인용입니다. 최종 점검은 앱에서 로그아웃 상태, 사용자 A, 사용자 B로 나눠 조회·추가·수정·삭제를 실행합니다. A가 B의 ID를 보냈을 때도 거부되는지, 공개하면 안 되는 열이 응답에 섞이지 않는지 함께 봅니다.

운영 반영은 정책과 필요한 GRANT, 앱 배포를 서로 맞춘 짧은 변경으로 진행합니다. 보안상 넓게 열린 정책을 발견했다면 편의를 위해 오래 유지하지 말고, 스테이징 검증과 롤백 SQL을 준비한 뒤 우선순위를 높여 닫습니다.