Huny's Dev Blog
huny.dev

Supabase 새 테이블 API 권한 변경: GRANT와 RLS를 함께 검증하기

10월 30일 기존 프로젝트 적용 일정에 맞춰 Supabase의 새 테이블 자동 노출 변경을 정리한다. 기존 테이블 영향 범위, 읽기 전용 권한 조회, 최소 GRANT와 RLS를 묶은 마이그레이션 예제를 살펴본다.

Hun Jang
Hun Jang Oct 30, 2026

데이터 접근을 명시적으로 허용하는 과정을 표현한 생성형 개념 이미지다. 실제 Supabase 화면이 아니다.

새 테이블은 만들어졌는데 Supabase 클라이언트에서 permission denied가 발생한다면, RLS 정책을 지우기 전에 테이블 권한부터 확인해야 한다. 객체에 접근할 수 있는 권한과, 그 안에서 볼 수 있는 행의 조건은 별개의 검사다.

Supabase는 2026년 4월 28일 새 테이블의 Data API 자동 노출 기본값 변경을 발표했고, 기존 프로젝트에 적용하는 날짜를 10월 30일로 안내했다. 지금 점검할 대상은 이후 마이그레이션이 만드는 테이블의 명시적 GRANT다. 개별 프로젝트의 전환 완료나 한국시간 기준 일괄 적용 시각을 확인했다는 뜻은 아니다. 공식 변경 공지

기존 테이블이 모두 차단되는 변경은 아니다

공지는 기존 테이블의 현재 권한이 유지된다고 명시한다. 변경된 기본값이 적용된 프로젝트에서 새로 만드는 public 테이블은 역할별 권한을 명시해야 Data API로 접근할 수 있다.

영향 경로는 supabase-js 같은 클라이언트가 사용하는 REST·GraphQL API다. 직접 Postgres 연결을 사용하는 ORM·앱 서버, self-hosted 배포는 이 관리형 서비스 변경의 대상이 아니다. auth·storage·realtime 및 사용자 정의 스키마의 기존 기본권한을 한꺼번에 바꾸는 공지도 아니다. 적용 범위

GRANT와 RLS를 나눠 읽는다

GRANT는 역할이 테이블에 SELECT·INSERT 같은 작업을 할 수 있는지 결정한다. RLS는 그 작업을 허용할 행을 고른다. SELECT 권한을 줬더라도 RLS가 허용하지 않은 다른 사용자의 행을 볼 수 있게 되는 것은 아니다.

반대로 정책만 만들어 놓고 SELECT 권한이 없다면 조회는 실패한다. RLS가 켜져 있는데 허용 정책이 없으면 일반 역할에는 기본적으로 행 접근이 거부된다. 테이블 소유자나 BYPASSRLS 역할로 성공한 조회를 일반 사용자의 검증으로 대신해서도 안 된다. PostgreSQL 행 보안

service_role의 RLS 우회도 객체 권한 확인을 생략하라는 뜻은 아니다. 서버 전용 비밀 키를 브라우저에 넣어 오류를 피하는 방식은 해결책이 될 수 없다. 클라이언트의 실제 사용자 역할과 JWT로 검사해야 한다. Supabase RLS 문서

먼저 읽기 전용으로 상태를 확인한다

다음은 공식 문서에 근거한 점검 예시이며, 이 글에서 실제 Supabase 프로젝트에 실행한 결과는 아니다. 테이블 이름을 자신의 대상으로 바꾼다.

has_table_privilege는 해당 역할의 객체 권한을 묻는다. 결과가 true라고 해서 RLS 조건, JWT, API의 노출 스키마 설정까지 통과했다는 뜻은 아니다. false도 별도로 부여된 컬럼 단위 SELECT 권한까지 부정하지는 않는다. 테이블이 없으면 후속 권한 조회가 오류를 낼 수 있으므로 존재 여부를 먼저 본다. 권한 조회 함수

RLS 활성화 여부도 별도로 확인한다.

기본권한은 “지금 이 테이블에 무엇이 허용되는가”와 다른 정보다. psql에서는 \ddp로 기본권한을 확인할 수 있다. ALTER DEFAULT PRIVILEGES는 미래 객체에 적용되며, 객체를 생성하는 현재 역할의 기본값을 따른다. 다른 역할의 기본값을 바꿨다고 마이그레이션 계정에도 자동 적용된다고 가정하면 안 된다. 기본권한 문서

새 테이블 예제: 생성·권한 정리·정책을 함께 배포한다

아래는 로그인한 사용자가 자신의 행만 읽는 새 테스트 테이블용 예제다. 기존 운영 테이블에 그대로 적용하는 수정 스크립트가 아니다. 테이블 생성 권한과 기존 Supabase 역할·auth.uid()가 있는 환경을 전제로 한다. 같은 이름이 이미 있으면 중단되도록 IF NOT EXISTS를 사용하지 않았다.

이 예제는 읽기 권한만 제공한다. 쓰기 기능이 필요하면 작업별 GRANT와 INSERT·UPDATE의 WITH CHECK를 따로 설계한다. 인증되지 않은 역할에는 접근을 열지 않는다. UUID 키를 사용하므로 이 예제에는 시퀀스 권한 설정이 없다. 다른 구조의 테이블이라면 시퀀스도 별도로 확인한다. 정책과 권한 구성

이렇게 한 트랜잭션에 묶는 것은 생성과 권한 설정이 분리된 배포를 줄이기 위한 제안이다. 조직에서 별도 역할 상속이나 사용자 정의 기본권한을 운용한다면 그 경로도 검토해야 한다.

오류 하나를 없애려고 전체 스키마를 열지 않는다

자동 노출 설정 해제가 모든 종류의 권한을 없앤다는 뜻은 아니다. 공식 보안 문서는 테이블의 TRUNCATE·REFERENCES·TRIGGER·MAINTAIN, 시퀀스 UPDATE 같은 권한이 남을 수 있고, 함수의 PUBLIC EXECUTE도 별도라고 설명한다. 따라서 “새 테이블 자동 노출을 껐으니 모든 접근이 차단됐다”는 결론은 과하다. Data API 보안

오류 코드 42501만으로 GRANT 누락을 단정하지 않는다. RLS의 쓰기 조건 위반도 같은 코드를 낼 수 있으므로 메시지와 실패 작업을 함께 읽는다. 오류 메시지가 GRANT를 제안하더라도 수신 역할과 작업이 앱 설계에 맞는지 먼저 본다. 모든 테이블을 anon에 개방하거나 RLS를 끄는 대신, 필요한 테이블의 필요한 작업만 허용한다.

배포 후 검증은 다음처럼 나눈다.

  • 비로그인 요청에는 의도하지 않은 데이터가 반환되지 않는가.
  • 사용자 A는 자신의 행만 읽고, 사용자 B의 행은 읽지 못하는가.
  • 허용하지 않은 INSERT·UPDATE·DELETE가 실패하는가.
  • 관리자 연결의 성공과 일반 사용자 Data API의 성공을 따로 확인했는가.
  • 기존 테이블뿐 아니라 이번 마이그레이션이 추가한 새 테이블도 검사했는가.

마이그레이션의 완료 조건에 스키마 생성뿐 아니라 역할별 GRANT와 행 단위 접근 검증을 포함하는 것이 이번 변경에 대응하는 실용적인 방법이다.

추천 글