ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Supabase][보안] service_role client인데 RLS error가 날 때 SSR 쿠키와 사용자 세션 덮어쓰기를 먼저 보는 법
    기타개발지식/풀스택개발 2026. 7. 2. 20:13

    IT 리서치 노트

    [Supabase][보안] service_role client인데 RLS error가 날 때 SSR 쿠키와 사용자 세션 덮어쓰기를 먼저 보는 법

    service_role key로 만든 Supabase client인데도 갑자기 RLS error가 나거나 데이터가 비어 돌아오면, 많은 팀이 policy SQL부터 다시 본다. 하지만 2026년 7월 2일 기준 Supabase troubleshooting 문서는 이 증상이 SSR 쿠키 세션, 직접 지정한 Authorization 헤더, auth 함수가 되돌린 사용자 세션 때문에 service_role 기본 Authorization이 덮이는 경우가 많다고 설명한다. 이 글은 service_role client인데도 관리자 우회가 안 되는 상황에서 무엇부터 추적해야 하는지 정리한 것이다.

    1. 개요

    결론부터 말하면 service_role client인데 RLS error가 나는 경우는 service_role 권한이 약해서가 아니라, 실제 요청의 Authorization 헤더가 service_role이 아니게 바뀐 경우가 많다. Supabase 문서는 RLS가 apikey가 아니라 Authorization 헤더 기준으로 적용된다고 설명하고, SSR client, 직접 지정한 Authorization 헤더, auth 함수 반환 세션이라는 세 가지 흔한 override 경로를 따로 적어 두고 있다.

    이미 RLS는 켰는데 데이터가 보이는 경우 service_role과 Authorization 헤더를 먼저 확인하는 글이 우회 쪽 증상을 다뤘다면, 이번 글은 반대 방향이다. 즉 'service_role인데 왜 관리자처럼 안 되지'라는 증상을 SSR 쿠키와 사용자 세션 덮어쓰기 기준으로 좁힌다.

    이번에 auth.admin.createUser와 signUp을 세션 반환과 확인 메일 기준으로 나눈 글을 같이 보면, 관리자 계정 생성 경로를 public signup 경로와 왜 분리해야 하는지까지 한 단계 넓게 정리할 수 있다.

    2. 어디서 실제로 막히는가

    실무에서 자주 겪는 증상은 세 가지다. 첫째, admin 작업이라 생각하고 service_role key를 넣었는데 예상치 못한 RLS error가 난다. 둘째, 서비스 계정 경로인데 select 결과가 비어 있거나 일부 row만 보인다. 셋째, auth 함수 호출 이후부터 같은 client의 동작이 달라진다.

    이때 가장 흔한 오해는 'service_role key를 createClient에 넣었으니 Authorization 헤더도 항상 그 값이겠지'라는 가정이다. 하지만 Supabase troubleshooting 문서는 Authorization 헤더가 service_role일 때만 항상 RLS를 우회한다고 적고, SSR client가 쿠키 기반 사용자 세션을 공유하거나, 직접 지정한 Authorization 헤더가 있거나, auth 함수가 새 사용자 세션을 되돌리면 헤더가 바뀔 수 있다고 설명한다.

    SSR guide와 Functions auth guide를 같이 보면 이 구조가 더 명확해진다. SSR helper는 기본적으로 쿠키를 통해 사용자 세션을 살리고, signed-in user가 호출한 function은 사용자 JWT를 Authorization 헤더로 보낸다. 따라서 'server에서 돌았다'는 사실만으로 service_role 경로라고 단정하면 안 된다. 어떤 helper를 썼고, 어떤 세션이 실렸고, 그 결과 Authorization 헤더가 무엇이 됐는지를 봐야 한다.

    • 증상: service_role client인데 RLS error가 난다.
    • 실패: apikey와 Authorization 헤더가 항상 같다고 가정한다.
    • 막힘: SSR helper와 admin client를 같은 인스턴스로 재사용한다.
    • 누락: auth 함수 호출 뒤 사용자 세션이 client에 남는 경로를 안 본다.
    증상 먼저 볼 곳 판단 기준
    SSR route에서만 RLS error 쿠키 공유 SSR client 사용자 세션이 Authorization을 덮는지 본다
    edge/server action에서만 결과가 다름 직접 지정한 Authorization 헤더 JWT가 service_role 대신 실렸는지 본다
    auth 함수 호출 뒤부터 비정상 세션 저장 여부 user session이 client에 남았는지 본다

    3. 실무에서 적용하는 순서

    가장 짧은 점검 순서는 다섯 단계다. 먼저 문제 요청의 Authorization 헤더 실제 값을 로그에서 조회하고 확인한다. 두 번째로 그 client가 SSR helper인지 순수 admin client인지 구분해 기록한다. 세 번째로 createClient 옵션에서 Authorization을 직접 덮어쓴 코드가 있는지 찾아 검증한다. 네 번째로 직전 auth 함수 호출이 사용자 세션을 남겼는지 조회한다. 마지막으로 그 뒤에야 grants와 RLS policy를 다시 본다.

    1. 문제 요청의 Authorization 헤더 실제 값을 조회해 남긴다.
    2. SSR client와 admin client를 분리해 확인한다.
    3. 직접 지정한 Authorization 옵션을 찾아 입력 경로를 검증한다.
    4. auth 함수 호출 직후 세션 저장 여부를 조회하고 확인한다.
    5. 마지막으로 grants와 RLS policy를 점검한다.

    핵심은 service_role key를 '정적 설정'이 아니라 '현재 요청이 실제로 들고 나간 Authorization 헤더'로 다시 보는 것이다. 구조상 세션 공유가 들어가는 client와 관리자 우회가 필요한 client를 분리해 두면, 같은 project에서도 증상 재현 시간이 크게 줄어든다.

    추적 메모 예시
    client_kind=ssr|admin
    authorization=JWT_OR_SERVICE_ROLE
    cookie_session_present=true|false
    manual_authorization_override=true|false
    auth_function_called=signUp|signIn|admin.createUser|none
    policy_checked_last=true

    이 메모를 남겨 두면 재발 때도 같은 client가 어떤 경로에서 뒤집히는지 빠르게 좁힐 수 있다. 특히 service_role key 노출 점검 글과 같이 보면, 공개 노출과 서버 내부 세션 덮어쓰기라는 두 가지 다른 위험을 따로 관리할 수 있다.

    4. 공식 문서와 예시 화면으로 확인하기

    첫 화면은 Supabase troubleshooting 문서의 핵심 문장이다. service role API key가 Authorization 헤더에 그대로 들어가면 RLS를 항상 우회한다는 전제가 먼저 맞아야 이후 증상이 설명된다.

    Supabase troubleshooting 문서는 service role Authorization 헤더면 RLS를 우회한다고 분명히 적고 있다.
    Supabase troubleshooting 문서는 service role Authorization 헤더면 RLS를 우회한다고 분명히 적고 있다.

    즉 service_role client인데도 RLS error가 난다면, 많은 경우 원인은 policy SQL이 아니라 Authorization 헤더가 다른 값으로 덮였다는 쪽에 있다. 이 글은 바로 그 덮어쓰기 경로를 좁히는 후속편이다.

    두 번째 자료는 Supabase가 직접 정리한 세 가지 override 경로다. SSR client, 직접 넣은 Authorization 헤더, auth 함수가 되돌린 사용자 세션이 service_role 기본값을 바꿀 수 있다는 뜻이다.

    Supabase troubleshooting 문서는 service_role client의 Authorization 헤더가 사용자 세션이나 다른 토큰으로 바뀌는 세 경로를 짚는다.
    Supabase troubleshooting 문서는 service_role client의 Authorization 헤더가 사용자 세션이나 다른 토큰으로 바뀌는 세 경로를 짚는다.

    중요한 점은 'service_role key를 넣었다'는 사실만으로 현재 요청의 권한을 판단할 수 없다는 것이다. 실제로는 어떤 client가 어떤 세션과 헤더를 들고 나갔는지가 더 중요하다.

    세 번째 화면은 SSR client guide다. 여기서는 SSR 환경에서 쿠키를 써 사용자 세션을 공유하도록 클라이언트를 구성한다고 설명한다.

    Supabase SSR guide는 서버사이드 client가 쿠키 기반 사용자 세션을 공유하도록 구성된다고 설명한다.
    Supabase SSR guide는 서버사이드 client가 쿠키 기반 사용자 세션을 공유하도록 구성된다고 설명한다.

    그래서 service_role 경로에 SSR helper를 그대로 가져오면 기대와 달리 사용자 세션이 Authorization 헤더를 덮어쓸 수 있다. troubleshooting 문서가 SSR service role client를 별도 경로로 분리하라고 하는 이유도 여기서 이어진다.

    네 번째 자료는 Edge Functions auth guide다. signed-in user 호출은 Authorization 헤더로 사용자 세션 JWT를 보낸다고 적혀 있다.

    Supabase Functions auth guide는 로그인된 사용자의 JWT가 Authorization 헤더로 전달된다고 설명한다.
    Supabase Functions auth guide는 로그인된 사용자의 JWT가 Authorization 헤더로 전달된다고 설명한다.

    이 구간을 보면 service_role client가 있어도 요청 경로 어딘가에서 사용자 JWT가 실리면 실제 권한은 그 토큰 기준으로 읽혀질 수 있다는 점이 분명해진다. 특히 edge function과 server action을 함께 쓰는 코드베이스에서 자주 헷갈린다.

    다섯 번째 화면은 API 보안 문서다. grants와 RLS는 같은 단계가 아니라는 점을 다시 확인하기 위해 넣었다.

    Supabase API security 문서는 grants와 RLS를 구분해 읽어야 한다고 설명한다.
    Supabase API security 문서는 grants와 RLS를 구분해 읽어야 한다고 설명한다.

    service_role client에서 RLS error가 났을 때도 결국 policy만 볼 일은 아니다. Authorization 헤더, 현재 role, grants, 세션 전달 경로를 먼저 나눠 봐야 한다.

    이 표는 service_role client가 생각보다 자주 뒤집히는 세 경로를 한 번에 묶은 것이다. 로그를 보기 전에 구조를 머리에 넣어 두면 재현 속도가 빨라진다.

    SSR 쿠키, 직접 지정한 Authorization, auth 함수 반환 세션이 service_role Authorization을 어떻게 덮는지 비교한 표다.
    SSR 쿠키, 직접 지정한 Authorization, auth 함수 반환 세션이 service_role Authorization을 어떻게 덮는지 비교한 표다.

    post 54가 'RLS는 켰는데 데이터가 보인다'는 증상을 헤더 기준으로 정리했다면, 이번 표는 반대로 'service_role인데 왜 RLS error가 나지'라는 증상을 세부 경로로 쪼갠 것이다.

    마지막 자료는 경로 분리 예시다. SSR client와 admin client를 분리하고, 사용자 생성도 auth 함수 대신 admin API 경로로 고정하는 편이 안전하다.

    SSR client와 admin client를 분리하고 auth 함수 반환 세션을 막는 예시다.
    SSR client와 admin client를 분리하고 auth 함수 반환 세션을 막는 예시다.

    이렇게 구조를 나눠 두면 Authorization 헤더가 언제 바뀌는지 추적이 쉬워진다. 이미 service_role key를 브라우저에 넣으면 안 되는 글을 봤다면, 이번 코드는 서버 안에서도 client 성격을 나눠야 하는 이유를 보여 준다.

    5. 주의사항과 리스크

    첫 번째 리스크는 SSR helper에 service_role을 그냥 넣는 것이다. 두 번째 리스크는 auth 함수가 돌려준 사용자 세션을 별도 admin 경로와 섞어 쓰는 것이다. 세 번째 리스크는 policy SQL만 계속 수정하면서 실제 Authorization 헤더를 안 보는 것이다.

    운영 전에는 최소한 브라우저 사용자 경로, SSR 경로, 순수 admin 경로 세 종류를 따로 찍어 보는 편이 좋다. 이 셋은 같은 테이블을 읽어도 정상 결과가 다를 수 있다. 같은 코드 파일에 있어도 같은 권한 경로가 아니다.

    • service_role key 자체보다 현재 Authorization 헤더를 우선 본다.
    • SSR helper와 admin client를 구조에서 분리한다.
    • auth 함수 반환 세션이 남는 경로를 따로 기록한다.

    6. 결론

    Supabase에서 service_role client인데도 RLS error가 난다면, 먼저 '왜 우회가 안 되지'보다 '지금 이 요청의 Authorization 헤더가 정말 service_role인가'를 확인해야 한다. SSR 쿠키 세션, 직접 지정한 Authorization, auth 함수 반환 세션이 service_role 기본값을 덮는 세 경로를 먼저 나누면 policy SQL보다 빠르게 원인을 좁힐 수 있다.

    • service_role client 문제는 Authorization 헤더 추적부터 시작한다.
    • SSR helper와 admin client를 섞지 않는다.
    • auth 함수 반환 세션은 관리자 경로와 분리한다.

    7. 참고 링크

    1. https://supabase.com/docs/guides/troubleshooting/why-is-my-service-role-key-client-getting-rls-errors-or-not-returning-data-7_1K9z
    2. https://supabase.com/docs/guides/auth/server-side/creating-a-client
    3. https://supabase.com/docs/guides/functions/auth
    4. https://supabase.com/docs/guides/api/securing-your-api
Designed by Tistory.