Supabase RLS 정책 작성과 검증: 배포 전에 확인할 정책 패턴

RLS 활성화, 기본 권한 회수, USING과 WITH CHECK, 성능, pgTAP 테스트

핵심 요약

  1. RLS를 켜고 public 테이블의 기본 권한을 회수한 뒤 필요한 권한만 다시 부여한다.
  2. USING은 대상 행을 거르고 WITH CHECK는 결과 행을 검사하며, UPDATE에는 둘 다 필요하다.
  3. 정책에 to authenticated를 지정하고 (select auth.uid())와 인덱스로 성능을 확보한다.
  4. service_role 키는 RLS를 우회하므로 서버 측에서만 쓰고 pgTAP으로 정책을 테스트한다.

Supabase는 브라우저에서 데이터베이스 API를 직접 호출하는 구조라, 테이블 접근 통제를 애플리케이션 서버가 아니라 데이터베이스의 행 수준 보안(RLS, Row Level Security)이 맡는다. AI 코딩 도구로 빠르게 만든 앱에서 RLS가 꺼져 있거나 정책이 느슨하면, 공개 키만으로 다른 사용자의 데이터를 읽을 수 있다. 이 글은 Supabase RLS 문서를 바탕으로 정책 작성 순서와 배포 전 검증 방법을 정리한다. 사내 도구 전반의 점검 항목은 바이브 코딩 사내 도구 보안 글에서 다뤘다.

1단계: RLS 켜고 기본 권한부터 줄이기

-- RLS 활성화
alter table public.notes enable row level security;

-- public 스키마의 새 테이블은 anon, authenticated에 모든 권한이 부여되므로 회수 후 필요한 것만 부여
revoke all on table public.notes from anon, authenticated;
grant select, insert, update, delete on table public.notes to authenticated;

문서에 따르면 RLS를 켜면 정책을 만들기 전까지 공개 키(publishable key)로는 API를 통해 아무 데이터도 볼 수 없다. 또 public 스키마에 새로 만든 테이블은 anon과 authenticated 역할에 모든 권한이 자동 부여되므로, 문서는 회수 후 선택적으로 다시 부여하라고 권한다. 이렇게 하면 RLS 정책과 GRANT 권한이 이중으로 접근을 제한한다.

2단계: 작업별 정책 작성하기

-- 조회: 자기 노트만
create policy "notes_select_own" on public.notes
for select to authenticated
using ( (select auth.uid()) = user_id );

-- 생성: user_id를 자기 ID로만 넣을 수 있음
create policy "notes_insert_own" on public.notes
for insert to authenticated
with check ( (select auth.uid()) = user_id );

-- 수정: 자기 행만 고르고, 수정 결과도 자기 행이어야 함
create policy "notes_update_own" on public.notes
for update to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

-- 삭제: 자기 행만
create policy "notes_delete_own" on public.notes
for delete to authenticated
using ( (select auth.uid()) = user_id );
절의미적용 작업
USING작업 대상이 될 수 있는 기존 행을 거른다SELECT, UPDATE, DELETE
WITH CHECK삽입·수정 결과로 만들어지는 행이 조건을 만족하는지 검사한다INSERT, UPDATE

UPDATE에 WITH CHECK를 빠뜨리면, 자기 행을 골라 user_id를 다른 사람 ID로 바꾸는 식으로 소유권을 넘기는 수정이 가능해질 수 있다. UPDATE 정책에는 두 절을 모두 쓴다.

3단계: 역할 지정과 성능 최적화

  • to authenticated 명시: 문서는 정책에 역할을 지정하면 anon 사용자에게는 정책 평가가 그 단계에서 멈춘다고 설명한다. 보안과 성능 모두에 유리하다.
  • (select auth.uid())로 감싸기: 함수 호출을 SELECT로 감싸면 문장 단위로 결과가 캐시되어 행마다 함수를 다시 호출하지 않는다.
  • 필터 컬럼 인덱스: 정책에서 비교하는 user_id 같은 컬럼에 인덱스를 만든다.
create index notes_user_id_idx on public.notes using btree (user_id);

4단계: service_role 키를 브라우저에 두지 않기

service_role(비밀 키)은 RLS를 우회한다. 문서는 비밀 키를 브라우저에서 쓰거나 고객에게 노출하지 말라고 강조한다. 관리 작업이 필요하면 서버 측 코드(서버 액션, Route Handler, Edge Function)에서만 쓰고, 환경 변수 이름에 NEXT_PUBLIC_ 같은 클라이언트 노출 접두사를 붙이지 않는다. 서버 측에서 비밀 키를 쓸 때는 RLS가 적용되지 않으므로 코드에서 직접 권한을 확인해야 한다.

5단계: pgTAP으로 정책 테스트하기

정책은 눈으로 읽는 것만으로는 빈틈을 찾기 어렵다. 문서는 supabase/tests/<테이블>_rls.test.sql에 pgTAP 테스트를 두고 supabase test db로 실행하는 방식을 안내하며, anon과 authenticated 두 역할로 네 가지 작업을 모두 시험하라고 권한다.

역할SELECTINSERTUPDATEDELETE
anon0행 반환실패0행 영향0행 영향
사용자 A(자기 행)자기 행만자기 ID로만 성공성공성공
사용자 A(B의 행)보이지 않음B의 ID로 삽입 실패0행 영향0행 영향

위 표가 기대 결과다. 특히 "다른 사용자의 ID로 INSERT"와 "자기 행의 user_id를 바꾸는 UPDATE"가 실패하는지 꼭 확인한다.

배포 전 최종 확인 순서

  1. 모든 public 테이블의 RLS 활성화 여부를 대시보드나 SQL로 확인한다.
  2. 정책이 없는 테이블, using (true)처럼 모두 허용하는 정책이 의도한 것인지 검토한다.
  3. pgTAP 테스트를 CI에서 실행해 정책 변경이 기존 보호를 깨지 않는지 막는다.
  4. 프론트엔드 번들에서 비밀 키 문자열이 검색되지 않는지 확인한다.

벡터 검색 테이블에도 같은 원칙이 적용된다. 검색 단계 권한 필터링은 사내 문서 RAG 접근 제어 글을, 벡터 DB 선택은 벡터 데이터베이스 비교 글을 참고한다.

초보자가 자주 실수하는 포인트

  • RLS를 켜지 않은 채 공개 키로 테이블을 노출하는 경우
  • UPDATE 정책에 WITH CHECK를 빠뜨려 소유권 변경이 가능해지는 경우
  • service_role 키를 클라이언트 환경 변수로 노출하는 경우
  • 정책에 역할을 지정하지 않아 anon 요청에도 정책이 평가되는 경우

체크리스트

  • 모든 public 테이블에 RLS가 켜져 있는가
  • anon·authenticated 기본 권한을 회수하고 필요한 것만 부여했는가
  • UPDATE 정책에 USING과 WITH CHECK가 모두 있는가
  • 정책 비교 컬럼에 인덱스가 있는가
  • 두 역할로 네 작업을 시험하는 pgTAP 테스트가 있는가

자주 묻는 질문

대시보드에서 만든 테이블도 RLS를 직접 켜야 하나요?

생성 방식과 관계없이 RLS 활성화 여부를 확인한다. SQL로 만든 테이블은 명시적으로 enable row level security를 실행해야 한다.

RLS가 있으면 서버 측 권한 확인은 필요 없나요?

공개 키로 접근하는 경로는 RLS가 막지만, 서버에서 service_role 키를 쓰는 코드는 RLS를 우회하므로 코드에서 권한을 확인해야 한다.

정책이 많아지면 느려지나요?

정책 조건은 쿼리마다 평가된다. 역할 지정, (select auth.uid()) 감싸기, 비교 컬럼 인덱스로 부담을 줄인다.

참고 자료 · 검증 기준

위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.

이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.