ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Supabase][인증] signing key rotation 뒤 verify_jwt false 함수에서 anonymous health check와 apikey-only route를 어떤 caller inventory로 다시 나누나
    기타개발지식/풀스택개발 2026. 8. 22. 09:18

    IT 리서치 노트

    [Supabase][인증] signing key rotation 뒤 verify_jwt false 함수에서 anonymous health check와 apikey-only route를 어떤 caller inventory로 다시 나누나

    Supabase signing key rotation 뒤 verify_jwt=false 함수만 남기고 보면 많은 팀이 모두 같은 '비사용자 경로'처럼 적는다. 하지만 2026년 8월 21일 기준 Supabase 공식 문서를 다시 보면 genuinely public health check는 `auth:'none'`과 `verify_jwt=false`를 쓰고, anonymous sign-in은 실제 사용자 JWT와 `authenticated` role을 쓰며, publishable key를 쓴다고 해서 곧 anonymous caller가 되지도 않는다. 이 글은 verify_jwt=false 함수에서 anonymous health check와 apikey-only route를 caller inventory로 어떻게 다시 쪼개야 rotation 회고가 짧아지는지 정리한다.

    1. 개요

    결론부터 말하면 Supabase verify_jwt=false 함수 inventory는 no-credential anonymous probe, provider-signed webhook, apikey-only internal route를 최소 세 줄로 나누는 편이 맞다. 여기에 anonymous sign-in 사용자는 아예 별도 줄로 빼야 한다. anonymous sign-in은 사용자 JWT와 authenticated role을 쓰기 때문이다.

    이 구분이 없으면 health check 통과를 근거로 apikey-only route까지 정상이라고 착각하거나, anonymous user JWT 흐름을 verify_jwt=false 경로에 잘못 섞어 RLS 해석을 틀리게 적는다. signing key rotation은 키 사건이고, caller inventory는 인증 경계 문서다. 둘은 함께 보되 같은 줄로 닫지 않는 편이 안전하다.

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

    실무에서 가장 흔한 실패는 verify_jwt=false 함수라는 이름만 남기고 누가 호출하는지 안 적는 것이다. health endpoint 하나와 internal sync endpoint 하나가 둘 다 verify_jwt=false일 수 있다. 하지만 health endpoint는 아무 credential도 기대하지 않고 status code만 보면 되고, internal sync endpoint는 적어도 apikey 헤더나 자체 signature를 확인해야 한다. 같은 토글 값을 봤다는 이유로 둘을 같은 줄에 넣으면 장애 보고가 바로 길어진다.

    두 번째로 많이 섞이는 축은 anonymous sign-in이다. Supabase 문서는 anonymous sign-in과 anon key를 다르게 설명한다. signInAnonymously()로 만든 사용자는 authenticated role과 사용자 JWT를 갖는다. 반면 anon key나 auth:'none' health check는 사용자를 만들지 않거나 아예 JWT를 기대하지 않는다. 그런데 운영 메모에는 종종 '게스트 경로'라는 말 하나로 묶여 들어간다. 이러면 어떤 경로는 RLS를 타고 어떤 경로는 안 타는데도 guest라는 단어만 남아 구분이 사라진다.

    세 번째 실패는 publishable key와 apikey-only route를 혼동하는 것이다. API keys 문서는 publishable key를 쓰더라도 로그인 사용자가 있으면 authenticated role을 쓸 수 있다고 설명한다. 즉 publishable key 호출은 '공개 브라우저 키'일 뿐, 자동으로 anonymous probe가 되는 것은 아니다. 반대로 apikey-only route는 보통 서버 또는 내부 자동화가 apikey 헤더와 자체 코드 경로를 함께 확인하는 경우를 뜻한다. 둘을 같이 적으면 브라우저 호출과 서버 내부 호출의 인증 표면이 섞인다.

    이 문제는 signing key rotation 뒤 더 커진다. 어떤 팀은 standby key와 revoke 시각만 적고 caller inventory를 안 남긴다. 어떤 팀은 verify_jwt=false 함수 목록만 남긴다. 또 어떤 팀은 anonymous sign-in 사용자까지 no-credential route로 오해한다. 이렇게 되면 특정 route만 깨져도 'rotation 실패'인지 'caller 구분 문서화 실패'인지가 분리되지 않는다.

    • 증상: health check는 통과했는데 internal route만 401 또는 custom auth error를 낸다.
    • 실패: verify_jwt=false 함수 전체를 anonymous route처럼 한 줄로 적는다.
    • 막힘: anonymous sign-in 사용자와 anon key public access를 guest라는 말 하나로 묶는다.
    • 누락: apikey header, custom signature, expected claim 값을 inventory에 안 남긴다.
    보이는 현상 섞이면 안 되는 대상 먼저 확인할 값
    health check는 200인데 internal sync가 실패한다 anonymous probe와 apikey-only route expected headers, auth code path, route owner
    게스트 사용자는 되는데 public endpoint는 열리지 않는다 anonymous sign-in user와 no-credential route JWT claim is_anonymous, postgres role, verify_jwt
    브라우저 호출과 서버 배치 호출 결과가 다르다 publishable key path와 apikey-only internal route apikey 종류, logged-in user 여부, internal signature

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

    가장 짧은 방법은 함수 이름이 아니라 caller class부터 inventory를 쓰는 것이다. 먼저 endpoint마다 누가 호출하는지 적는다. 그다음 expected credential을 적는다. 마지막으로 close condition을 적는다. verify_jwt=false라는 설정값은 그 뒤에 보조 필드로 남긴다. 이 순서로 적으면 같은 verify_jwt=false라도 anonymous probe와 apikey-only route가 전혀 다른 줄로 나뉜다.

    1. endpoint별 caller class를 적고 health check인지 webhook인지 internal route인지 구분한다.
    2. JWT, apikey, signature, no-credential 중 무엇을 기대하는지 확인한다.
    3. anonymous sign-in 사용자는 별도 user-JWT 경로로 적고 is_anonymous claim을 남긴다.
    4. route owner, first passed request, revoke dependency를 같이 기록한다.
    5. rotation 완료 메모와 caller inventory 메모를 분리해서 저장한다.
    • 호출자를 확인하고 헤더를 비교하고 claim을 조회하고 route owner를 기록한다.
    • health check를 실행하고 status code를 확인하고 no-user-JWT 가정을 검증한다.
    • apikey-only route를 실행하고 apikey header를 확인하고 custom signature 검증 코드를 읽는다.
    • anonymous sign-in 테스트를 실행하고 authenticated role과 is_anonymous claim을 비교한다.
    • 첫 성공 시각을 저장하고 legacy key revoke 시각과 별도 칸으로 분리한다.
    • 오류 응답을 저장하고 route별 fallback 계획을 남기고 회귀 여부를 점검한다.

    이때 inventory 필드는 작게 유지하는 편이 좋다. caller class, expected credential, close condition, route owner, first passed request 정도면 충분하다. 너무 많은 운영 필드를 한 줄에 몰아 넣으면 다시 읽지 않는다. 반대로 이 최소 필드조차 없으면 verify_jwt=false 함수를 전부 같은 부류처럼 다루게 된다.

    특히 브라우저 쪽 guest 흐름이 있다면 anonymous sign-in 사용자를 public endpoint와 절대 같은 줄에 두지 않는 편이 좋다. anonymous sign-in은 실제 user JWT를 가진다. 그러므로 health check 성공으로 anonymous user flow를 닫을 수 없고, 반대로 anonymous user flow 성공으로 health check openness를 닫을 수도 없다. 전자는 RLS와 role 검증이 필요하고 후자는 credential 부재가 맞는지 확인해야 한다.

    운영 메모에는 어떤 명령을 실행했고 어떤 헤더를 확인했고 어떤 로그를 봤고 어떤 응답을 저장했는지 같이 적어 두는 편이 좋다. 그래야 나중에 누군가가 같은 함수를 다시 검증할 때 '왜 이 route는 verify_jwt=false인데도 public route가 아니지?' 같은 질문에 즉시 답할 수 있다.

    실무 메모 예시
    route=/functions/v1/health
    caller_class=anonymous_probe
    verify_jwt=false
    expected_credentials=none
    close_when=status_200_seen
    
    route=/functions/v1/internal-sync
    caller_class=apikey_only
    verify_jwt=false
    expected_credentials=apikey+x-internal-signature
    close_when=header_check_ok+auth_code_path_ok
    
    route=/auth/guest-bootstrap
    caller_class=anonymous_user_jwt
    verify_jwt=true
    close_when=jwt_ok+rls_scope_ok+is_anonymous=true

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

    첫 공식 자료는 Supabase Edge Functions auth 문서다. 여기서는 genuinely public function, 즉 health check 같은 경로에 `auth: 'none'`과 `verify_jwt = false`를 함께 쓰라고 적는다.

    Supabase는 anonymous health check 같은 genuinely public function에 `auth: 'none'`과 `verify_jwt = false`를 함께 쓰라고 설명한다.
    Supabase는 anonymous health check 같은 genuinely public function에 `auth: 'none'`과 `verify_jwt = false`를 함께 쓰라고 설명한다.

    이 문장은 verify_jwt=false 함수가 모두 같은 유형이 아니라는 출발점이다. 어떤 함수는 정말 아무 사용자 자격 증명도 기대하지 않고, 어떤 함수는 JWT 대신 다른 헤더나 자체 서명을 기대한다. rotation 뒤 caller inventory가 필요한 이유가 바로 여기서 생긴다.

    두 번째 자료는 Anonymous Sign-Ins 문서다. Supabase는 anonymous user와 anon key를 분리해서 설명한다. `signInAnonymously()`는 사용자를 만들고 `authenticated` role을 쓰지만, anon key는 public access용 `anonymous` role을 쓴다.

    Supabase 문서는 anonymous user와 anon key가 다르고, anonymous sign-in은 `authenticated` role을 쓴다고 설명한다.
    Supabase 문서는 anonymous user와 anon key가 다르고, anonymous sign-in은 `authenticated` role을 쓴다고 설명한다.

    즉 verify_jwt=false health check를 anonymous user 호출과 같은 줄에 적으면 바로 틀린다. anonymous sign-in 사용자는 사용자 JWT를 갖고 있고, no-credential health check는 아예 JWT를 기대하지 않는다. inventory에서 둘을 따로 적지 않으면 회전 뒤 RLS나 role 해석이 섞인다.

    세 번째 공식 자료는 API keys 문서다. Supabase는 publishable key를 쓰더라도 사용자가 로그인돼 있으면 `authenticated` role을 쓸 수 있다고 적는다.

    publishable key를 쓴다고 곧 anonymous caller가 되는 것은 아니며, 로그인 사용자 JWT와 같이 동작할 수 있다고 Supabase가 설명한다.
    publishable key를 쓴다고 곧 anonymous caller가 되는 것은 아니며, 로그인 사용자 JWT와 같이 동작할 수 있다고 Supabase가 설명한다.

    이 점을 빼먹으면 apikey-only route inventory에 publishable key 브라우저 호출까지 잘못 끌고 들어가게 된다. apikey-only route는 보통 서버나 내부 호출이 별도 헤더와 코드 경로를 확인하는 경우를 뜻하고, 로그인 사용자 호출은 또 다른 inventory 줄이어야 한다.

    실무에서는 함수 코드보다 caller inventory 표가 먼저다. verify_jwt=false라고만 쓰면 anonymous health check, external webhook, apikey-only route, anonymous sign-in 사용자가 모두 한 덩어리처럼 보인다.

    verify_jwt=false 주변 호출자를 anonymous health check, anonymous sign-in, apikey-only route로 분리한 caller inventory 표다.
    verify_jwt=false 주변 호출자를 anonymous health check, anonymous sign-in, apikey-only route로 분리한 caller inventory 표다.

    이미 Schedule Functions와 pg_net·Database Webhooks inventory 글과 verify_jwt=false와 signed-in 함수 종료 기준 글을 읽었다면, 이번 표는 verify_jwt=false 내부에서도 caller를 더 잘게 나누는 후속편이다.

    마지막 자료는 회전 뒤 inventory 메모 예시다. 함수별로 누가 호출하고 어떤 헤더를 기대하는지 한 줄씩 남기지 않으면 key rotation 성공과 caller-path 성공을 분리해서 말하기 어렵다.

    health check와 apikey-only route를 따로 남기는 최소 inventory 메모 예시다.
    health check와 apikey-only route를 따로 남기는 최소 inventory 메모 예시다.

    이 정도 메모면 revoke 시점이 와도 어떤 경로가 user JWT인지, 어떤 경로가 apikey-only인지, 어떤 경로가 그냥 anonymous probe인지 빠르게 다시 확인할 수 있다. cache purge와 JWKS 대기 시간은 cache purge·session invalidation 글과 verifier cache window 글과 연결해 보면 된다.

    5. 주의사항과 리스크

    첫 번째 리스크는 verify_jwt=false라는 공통점만 보고 anonymous probe와 apikey-only route를 합치는 것이다. 두 번째 리스크는 anonymous sign-in 사용자를 anon key public access와 같은 말로 부르는 것이다. 세 번째 리스크는 publishable key를 쓴다고 자동으로 anonymous caller라고 오해하는 것이다.

    운영 전에 확인할 때는 최소한 caller class, expected credential, close condition 세 필드는 고정해 두는 편이 좋다. 이 세 필드가 없으면 health check, internal route, user JWT path가 다시 섞인다.

    • verify_jwt=false는 caller class가 아니라 설정값이다.
    • anonymous sign-in 사용자는 user JWT와 authenticated role을 가진다.
    • apikey-only route는 헤더 검증과 코드 경로를 같이 남겨야 한다.

    6. 결론

    Supabase signing key rotation 뒤 verify_jwt=false 함수를 정리할 때는 함수 이름보다 caller inventory를 먼저 나누는 편이 맞다. anonymous health check, provider-signed webhook, apikey-only route, anonymous user JWT path를 따로 적어 두면 같은 verify_jwt=false라도 서로 다른 인증 표면을 한눈에 복원할 수 있다.

    • health check는 no-credential anonymous probe로 적는다.
    • apikey-only route는 header와 auth code path를 같이 적는다.
    • anonymous sign-in 사용자는 별도 user-JWT inventory 줄로 분리한다.

    7. 참고 링크

    1. https://supabase.com/docs/guides/functions/auth
    2. https://supabase.com/docs/guides/auth/auth-anonymous
    3. https://supabase.com/docs/guides/getting-started/api-keys
    4. https://supabase.com/docs/guides/auth/jwts
    5. https://supabase.com/docs/guides/getting-started/migrating-to-new-api-keys
Designed by Tistory.