ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Supabase Edge Functions][보안] verify_jwt=false 엔드포인트와 backend secret key를 같이 쓸 때 apikey와 Authorization을 어디서 먼저 나누나
    기타개발지식/풀스택개발 2026. 8. 2. 09:14

    IT 리서치 노트

    [Supabase Edge Functions][보안] verify_jwt=false 엔드포인트와 backend secret key를 같이 쓸 때 apikey와 Authorization을 어디서 먼저 나누나

    Supabase Edge Functions에서 `verify_jwt=false`를 쓰기 시작하면 많은 팀이 그 함수가 그냥 '인증 없는 엔드포인트'가 되었다고 오해한다. 하지만 2026년 8월 1일 기준 Supabase 공식 문서를 다시 보면, cron worker, `pg_net`, Database Webhooks 같은 backend caller는 user JWT가 아니라 `apikey` header와 secret key 경로로 다뤄야 하고, API key를 `Authorization`으로 보내면 함수 전에 막힐 수 있다. 이 글은 verify_jwt=false 엔드포인트와 backend secret key를 같이 쓸 때 apikey와 Authorization을 어디서 먼저 나눠야 덜 꼬이는지 정리한 것이다.

    1. 개요

    결론부터 말하면 verify_jwt=false 함수는 '누구나 접근 가능'한 함수가 아니라, browser JWT 경로와 backend secret key 경로를 분리하기 위한 예외 경로로 보는 편이 맞다. 브라우저 사용자는 Authorization Bearer user JWT를 유지하고, cron worker·pg_net·Database Webhooks 같은 backend caller만 apikey header와 secret key 경로로 분리해야 한다.

    즉 먼저 나눌 것은 route 목적과 caller 종류다. 이 구분이 없으면 verify_jwt=false를 켠 뒤에도 Authorization header를 잘못 보내거나, 반대로 브라우저 경로에 secret key caller를 섞어 두는 식으로 더 위험한 상태가 생긴다.

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

    현장에서 먼저 막히는 지점은 세 가지다. 첫째, verify_jwt=false를 켠 뒤 브라우저와 backend caller를 같은 route에 붙인다. 둘째, 새 secret key를 도입하고도 webhook이나 pg_net가 예전처럼 Authorization Bearer 헤더를 보내 handler 전에 실패한다. 셋째, secret key caller 전용 함수인데도 어떤 caller가 허용되는지 inventory가 없어 old key 삭제나 route audit이 느려진다.

    Supabase 문서는 secret key caller가 apikey header를 쓴다고 설명하고, auth headers 문서는 API key가 JWT가 아니므로 Authorization 검사에 통과하지 않는다고 말한다. migration guide는 Database Webhooks 설정에서 Authorization을 지우고 apikey header로 바꾸라고 직접 안내한다. 이 셋을 같이 보면 verify_jwt=false 함수 설계는 단순 플래그 조정이 아니라, caller 종류와 header 종류를 동시에 분리하는 작업이라는 점이 분명해진다.

    • 증상: verify_jwt=false 함수인데 webhook만 401 또는 403을 낸다.
    • 실패: secret key caller를 Authorization Bearer로 계속 보낸다.
    • 막힘: browser JWT 경로와 backend secret route를 같은 함수에 합친다.
    • 누락: caller inventory 없이 old key와 route 삭제 시점을 못 잡는다.
    질문 먼저 볼 곳 실무 판단
    누가 이 함수를 호출하나 browser, webhook, worker, pg_net 분류 browser와 backend route를 분리한다
    왜 함수 전에 막히나 Authorization인지 apikey인지 secret key는 apikey 경로를 우선 본다
    언제 old key를 삭제하나 caller inventory와 route inventory backend caller가 모두 전환된 뒤 삭제한다

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

    운영 순서는 다섯 단계가 가장 실용적이다. 먼저 Edge Function route를 browser JWT용과 backend secret용으로 나눈다. 다음으로 backend secret용 함수만 verify_jwt=false 또는 auth:'secret' 경로로 제한한다. 세 번째로 cron worker, pg_net, Database Webhooks의 호출 헤더를 Authorization에서 apikey로 교체한다. 네 번째로 ctx.supabaseAdmin 사용 범위를 backend-only route 안으로 가둔다. 마지막으로 caller inventory와 old key 사용 로그를 확인한 뒤 이전 route와 key를 제거한다.

    실제 작업에서는 Functions 설정에서 대상 함수의 인증 모드를 확인하고, 브라우저가 접근하는 함수는 기본 JWT 검증을 유지한다. backend caller 전용 함수는 route 이름과 문서에 용도를 명시하고, Dashboard webhook 설정 화면에서 HTTP header 항목을 열어 apikey 값을 입력하고 저장한다. pg_net 호출이나 다른 worker도 같은 방식으로 header를 맞추고, 함수 실행 로그에서 caller별 2xx와 4xx 비율을 따로 조회한다. 배포 직후에는 콘솔에서 요청 응답 코드, 오류 로그, header 설정값, old key 사용 로그를 같이 확인해야 cutover 누락을 빨리 찾을 수 있다.

    1. browser JWT route와 backend secret route를 분리한다.
    2. backend secret route만 verify_jwt=false 또는 auth:'secret'로 둔다.
    3. webhook·pg_net·worker header를 apikey로 교체한다.
    4. ctx.supabaseAdmin은 backend-only route 안으로 제한한다.
    5. caller inventory 확인 뒤 old key와 old route를 제거한다.

    예를 들어 사용자 요청이 직접 들어오는 /functions/v1/profile 같은 경로는 Bearer user JWT를 유지하고, Database Webhooks나 pg_net가 호출하는 /functions/v1/outbox-dispatch 같은 경로만 secret caller 전용으로 나누는 식이다. 이 분리가 없으면 secret key caller를 열어 놓은 route에 브라우저 트래픽이 섞이거나, 브라우저 인증 실패를 backend auth 문제로 오해하기 쉽다.

    route=/functions/v1/outbox-dispatch
    caller_kind=pg_net|database_webhook|worker
    auth_mode=secret
    header_kind=apikey
    ctx_supabase_admin=true
    browser_access=false
    
    route=/functions/v1/profile
    caller_kind=browser
    auth_mode=jwt
    header_kind=authorization_bearer
    ctx_supabase_admin=false

    이 메모가 있으면 verify_jwt=false 함수가 왜 필요한지, 어떤 caller만 허용하는지, old key를 언제 지울 수 있는지 한 화면에서 설명할 수 있다. 특히 route별 권한, header 설정, 배포 순서, 로그 조회 지점을 같이 적어 두면 운영자가 파일과 문서를 다시 열었을 때도 같은 절차를 반복하기 쉽다.

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

    첫 공식 화면은 Supabase가 secret key caller를 어떤 경로로 보는지 설명하는 구간이다. pg_net, worker, 다른 Edge Function 같은 backend caller는 user JWT가 아니라 apikey header를 쓴다고 분명히 적혀 있다.

    Supabase는 backend caller가 user JWT 대신 apikey header와 secret key를 쓴다고 설명한다.
    Supabase는 backend caller가 user JWT 대신 apikey header와 secret key를 쓴다고 설명한다.

    즉 verify_jwt=false 또는 auth:'secret' 경로를 열었다면, 브라우저 세션과 같은 규칙으로 보지 말고 backend caller 경계를 먼저 자르는 편이 맞다.

    두 번째 화면은 가장 자주 헷갈리는 부분이다. publishable key와 secret key는 JWT가 아니므로 Authorization header로 보내면 핸들러 전에 실패할 수 있다.

    Supabase는 API key가 JWT가 아니므로 Authorization header 검사에 통과하지 않는다고 설명한다.
    Supabase는 API key가 JWT가 아니므로 Authorization header 검사에 통과하지 않는다고 설명한다.

    이 문장이 verify_jwt=false 경계의 핵심이다. secret key caller를 열어도 header를 잘못 보내면 함수 로직에 도달하기 전에 막힌다.

    세 번째 공식 화면은 새 API key 체계 migration guide다. Database Webhooks 같은 Dashboard 설정에서는 Authorization을 지우고 apikey header를 추가하라고 직접 적고 있다.

    Supabase migration guide는 Database Webhooks에서 Authorization 대신 apikey header를 쓰라고 안내한다.
    Supabase migration guide는 Database Webhooks에서 Authorization 대신 apikey header를 쓰라고 안내한다.

    따라서 verify_jwt=false 함수가 있다고 해서 예전 Bearer 헤더를 계속 유지하면 안 된다. caller 종류와 header 종류를 동시에 바꿔야 cutover가 끝난다.

    네 번째 공식 화면은 Database Webhooks 설명이다. webhook은 브라우저가 아니라 database-side backend caller이므로 인증 경계를 따로 보는 편이 자연스럽다.

    Database Webhooks는 database event가 다른 시스템으로 payload를 보내는 backend 경로다.
    Database Webhooks는 database event가 다른 시스템으로 payload를 보내는 backend 경로다.

    이 화면을 보면 verify_jwt=false 함수를 열더라도 모든 caller를 허용하는 것이 아니라, backend automation 경로를 분리하는 작업이라는 점이 분명해진다.

    다섯 번째 자료는 caller별 인증 경계표다. browser JWT, backend secret key, webhook/pg_net 경로를 한 표에 놓아야 verify_jwt 설정을 어디에만 적용할지 빨리 정할 수 있다.

    Supabase Edge Functions에서 apikey, Authorization, verify_jwt 경계를 나누는 표다.
    Supabase Edge Functions에서 apikey, Authorization, verify_jwt 경계를 나누는 표다.

    이미 backend별 secret key rotation 글이 key 경계를 다뤘다면, 이번 표는 함수 호출 시점의 인증 경계를 더 좁힌다.

    마지막 자료는 secret caller 전용 함수 예시다. verify_jwt=false만 적는 것보다, 어떤 caller를 허용하는지 코드와 route 이름에서 드러나는 편이 안전하다.

    Supabase secret key caller 전용 Edge Function 예시다.
    Supabase secret key caller 전용 Edge Function 예시다.

    또 legacy key에서 publishable·secret key로 옮기는 글과 같이 보면 key migration과 function auth boundary를 한 번에 묶기 쉽다.

    5. 주의사항과 리스크

    첫 번째 리스크는 verify_jwt=false를 사실상 public endpoint처럼 운영하는 것이다. route 분리가 없으면 secret key caller 경계가 무너지기 쉽다. 두 번째는 secret key가 JWT가 아니라는 사실을 놓쳐 Authorization Bearer로 계속 보내는 것이다. 이 경우 함수 코드에는 문제가 없어도 요청은 handler 전에 실패한다. 세 번째는 backend caller inventory 없이 key rotation만 반복해 어떤 webhook과 worker가 아직 old route를 쓰는지 모르게 되는 것이다.

    운영 전에 확인할 때는 route 이름, caller 종류, auth 모드, header 종류, old key 사용량 다섯 칸을 같은 표에 두는 편이 좋다. 여기에 함수 배포 시각, 최근 오류 응답, webhook 설정 화면 위치, pg_net 호출 로그까지 같이 적으면 권한 경계와 cutover 진행률을 한 번에 확인할 수 있다. 이 다섯 칸이 없으면 브라우저 인증 문제와 backend auth cutover 문제를 계속 섞게 된다.

    • verify_jwt=false는 backend secret caller 전용 예외로 본다.
    • API key caller는 apikey header 경로를 먼저 확인한다.
    • ctx.supabaseAdmin은 backend-only 함수 안으로 제한한다.

    6. 결론

    Supabase Edge Functions auth 설계의 핵심은 verify_jwt=false를 넓게 푸는 일이 아니라, browser JWT와 backend secret caller를 정확히 분리하는 일이다. route를 나누고, backend caller는 apikey header와 secret route로 모으고, old key와 old route를 순차 제거하면 key migration과 function auth가 한 번에 정리된다.

    같은 가지의 다음 점검은 어떤 worker, webhook, pg_net 호출이 아직 old header를 보내는지다. 이 inventory만 정확하면 key rotation incident와 function auth incident를 훨씬 짧게 닫을 수 있다.

    그 inventory를 실제 runbook으로 좁힌 후속편으로는 pg_net와 Database Webhooks가 old Authorization 헤더를 계속 보낼 때 apikey cutover를 다시 나누는 글이 있다. route 경계가 정리된 다음에는 caller별 header 잔여분을 이 글 기준으로 닫는 편이 빠르다.

    • browser와 backend caller는 route부터 분리한다.
    • secret key caller는 apikey header를 기본값으로 본다.
    • verify_jwt=false는 backend-only 예외로 운영한다.

    7. 참고 링크

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