Supabase는 빠른 백엔드 구축을 가능하게 하지만, 잘못된 설정 하나가 전체 데이터베이스를 노출시킬 수 있다. RLS(Row Level Security) 비활성화와 Service Key 클라이언트 노출이 가장 흔한 실수다. 이 체크리스트를 통해 배포 전 필수 보안 항목을 점검하자.
클라이언트 (브라우저/앱)
│
│ anon key (공개 가능)
▼
Supabase API Gateway
│
│ JWT 검증 → RLS 정책 적용
▼
PostgreSQL (RLS ON)
▲
│ service_role key (서버 전용)
│
백엔드 서버 (Next.js API Route 등)
anon key: 비로그인 사용자용 공개 키. RLS가 적용된 테이블에만 접근 가능. service_role key: RLS를 우회하는 관리자 키로 절대 클라이언트에 노출 금지.
모든 테이블은 생성 즉시 RLS를 활성화해야 한다. 활성화하지 않으면 anon key만으로 전체 데이터를 읽고 쓸 수 있다.
-- 테이블 생성 후 즉시 실행
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 본인 데이터만 조회 허용
CREATE POLICY "본인 게시물 조회"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- 본인 데이터만 수정 허용
CREATE POLICY "본인 게시물 수정"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
auth.uid는 Supabase가 JWT에서 추출한 현재 사용자 ID다. RLS 정책에서 이 함수를 기준으로 행 단위 접근을 제어한다.
// 잘못된 예 — 클라이언트 코드에 service key 노출
const supabase = createClient(url, process.env.NEXT_PUBLIC_SERVICE_KEY);
// 올바른 예 — 서버 사이드(API Route)에서만 사용
// pages/api/admin.ts
import { createClient } from "@supabase/supabase-js";
const adminClient = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // NEXT_PUBLIC_ 접두사 없음
);
NEXT_PUBLIC_ 접두사가 붙은 환경변수는 번들에 포함되어 브라우저에서 노출된다. Service Key는 절대 NEXT_PUBLIC_ 접두사를 사용하지 않는다.
Supabase 클라이언트는 기본적으로 HTTPS 엔드포인트를 사용하지만, 자체 호스팅 환경에서는 명시적으로 강제해야 한다.
const supabase = createClient(url, anonKey, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true,
},
global: {
headers: { "x-custom-header": "my-app" },
},
});
JWT 만료 시간은 Supabase 대시보드 Authentication → Settings → JWT expiry에서 조정한다. 기본값 3600초(1시간)를 프로덕션 요건에 맞게 단축하는 것을 권장한다.
Storage 버킷도 RLS와 동일한 정책 시스템을 사용한다. 버킷을 public으로 설정하면 정책 없이 전체 공개된다.
-- 인증된 사용자만 자신의 파일 업로드 허용
CREATE POLICY "본인 파일 업로드"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars' AND
auth.uid()::text = (storage.foldername(name))[1]
);
| 항목 | 확인 방법 |
|---|---|
| 모든 테이블 RLS 활성화 | Dashboard → Table Editor → 자물쇠 아이콘 확인 |
| anon key로 민감 데이터 접근 불가 | Postman으로 anon key 요청 후 403 확인 |
| Service Key 환경변수 서버 전용 | NEXT_PUBLIC_ 접두사 없음 확인 |
| Storage 버킷 public 설정 검토 | 불필요한 public 버킷 제거 |
| JWT 만료 시간 설정 | 1시간 이하 권장 |
| Database 직접 접근 IP 제한 | Dashboard → Database → Connection pooling |
RLS 없는 Supabase는 인터넷에 열린 데이터베이스와 같다. 테이블 생성과 동시에 RLS를 활성화하고, Service Key는 서버 경계 밖으로 나가지 않도록 아키텍처 수준에서 보장해야 한다.