Published on

GCP Cloud Run 403 해결 - IAM·Invoker 권한 7단계

Authors

Cloud Run을 배포했는데 브라우저나 curl로 호출하면 바로 403이 떨어지는 경우가 있습니다. 로그는 멀쩡해 보이고, 서비스도 READY 인데도 말이죠. 이 문제의 핵심은 거의 항상 누가 호출했는지(인증 주체)그 주체가 roles/run.invoker 를 갖고 있는지(IAM 바인딩) 가 맞지 않는 데 있습니다.

이 글은 Cloud Run 403을 7단계 체크리스트로 쪼개서, “지금 내 요청이 어떤 인증으로 들어왔고, 어디에서 막혔는지”를 빠르게 확정하는 방식으로 정리합니다. AWS/EKS에서 IAM이 꼬여 전체 인증이 무너지는 상황과 비슷한 결로, 권한·주체·토큰을 분리해서 보는 게 가장 빠릅니다. 비슷한 사고 복구 관점은 EKS OIDC Provider 삭제로 IRSA 전부 실패했을 때 복구도 참고가 됩니다.

0) 403부터 분류: 어떤 403인가

Cloud Run에서 “403”은 하나가 아닙니다. 최소한 아래 3가지는 구분해야 합니다.

  • Cloud Run 레이어에서 차단: Invoker 권한 부족, 인증 토큰 문제
  • 게이트웨이/프록시 레이어에서 차단: API Gateway, IAP, Load Balancer, Cloud Armor 등
  • 애플리케이션에서 403 반환: 앱 코드가 직접 403을 리턴

가장 먼저 할 일은 “403이 Cloud Run에서 난 건지, 앱에서 난 건지”를 확인하는 것입니다.

빠른 판별 팁

  • Cloud Run 콘솔의 Request logs 에 요청이 아예 안 찍히면, 앞단(게이트웨이/로드밸런서)에서 막혔을 가능성이 큽니다.
  • 요청 로그가 찍히는데 status가 403이면, Cloud Run 인증/권한 또는 앱 로직을 의심합니다.

아래 단계는 Cloud Run 자체 403(Invoker/IAM/토큰)부터 우선 해결하도록 구성했습니다.

1단계: 서비스가 “인증 필요”인지부터 확정

Cloud Run은 크게 두 모드가 있습니다.

  • 인증 필요(기본): 호출자는 ID 토큰을 포함해야 하고, 해당 주체에 roles/run.invoker 가 있어야 합니다.
  • 인증 없이 허용(공개): allUsersroles/run.invoker 를 부여한 상태

아래 명령으로 현재 설정을 확인합니다.

gcloud run services describe SERVICE_NAME \
  --region=REGION \
  --format="value(status.url)"

인증 필요 여부는 URL만으로는 확정이 안 됩니다. 대신 IAM policy를 봐야 합니다.

gcloud run services get-iam-policy SERVICE_NAME \
  --region=REGION

여기서 roles/run.invoker 바인딩에 allUsers 가 있으면 공개 호출이 가능합니다.

2단계: “Invoker는 어디에 붙여야 하는가”를 확인

Cloud Run 호출 권한은 보통 서비스 단위로 부여합니다. 하지만 운영 중 구성에 따라 혼동이 생깁니다.

  • 원칙: gcloud run services add-iam-policy-binding 로 서비스에 부여
  • 실수: 프로젝트 레벨 IAM만 만지고 서비스 IAM을 안 만짐
  • 실수: 다른 리전에 있는 동명 서비스에 부여

서비스/리전을 명확히 해서 바인딩합니다.

gcloud run services add-iam-policy-binding SERVICE_NAME \
  --region=REGION \
  --member="user:me@example.com" \
  --role="roles/run.invoker"

서비스 계정에 부여하려면:

gcloud run services add-iam-policy-binding SERVICE_NAME \
  --region=REGION \
  --member="serviceAccount:CALLER_SA@PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/run.invoker"

3단계: 호출 주체(Principal)가 “누구로 인식되는지”를 확정

403 디버깅에서 가장 흔한 착각은 “내가 로그인했으니 내 권한으로 호출되겠지”입니다. 실제로는 호출 경로에 따라 주체가 달라집니다.

  • 로컬 curl + ID 토큰: 토큰의 sub/email 주체
  • Cloud Scheduler: 스케줄러가 사용하는 OIDC 서비스 계정
  • Cloud Tasks: 태스크가 사용하는 OIDC 서비스 계정
  • API Gateway: 게이트웨이가 백엔드 호출에 사용하는 주체(설정에 따라 다름)
  • 다른 Cloud Run/Cloud Functions: 런타임 서비스 계정

즉, Invoker를 “내 사용자”에 줬는데 실제 호출은 “서비스 계정”으로 들어오면 403입니다.

로컬에서 내 계정으로 호출 테스트

URL="https://YOUR_SERVICE-xxxxx.a.run.app"
TOKEN=$(gcloud auth print-identity-token)

curl -i "$URL" \
  -H "Authorization: Bearer $TOKEN"
  • 여기서 403이면, 내 계정에 Invoker가 없거나 토큰/audience가 틀렸을 가능성이 큽니다.

4단계: ID 토큰의 audience가 URL과 일치하는지 확인

Cloud Run은 ID 토큰의 aud 가 서비스 URL과 맞아야 통과합니다. 이게 어긋나면 권한이 있어도 403이 납니다.

gcloud auth print-identity-token 은 기본적으로 현재 계정 기반 토큰을 찍지만, 환경에 따라 aud 불일치가 생길 수 있습니다. 가장 안전한 방식은 --audiences 를 명시하는 것입니다.

URL="https://YOUR_SERVICE-xxxxx.a.run.app"
TOKEN=$(gcloud auth print-identity-token --audiences="$URL")

curl -i "$URL" \
  -H "Authorization: Bearer $TOKEN"

토큰을 까서 확인하고 싶다면(로컬 디버그용):

python - << 'PY'
import os, json, base64

t = os.environ.get('TOKEN','')
parts = t.split('.')
if len(parts) != 3:
    raise SystemExit('not a jwt')

def b64url(s):
    s += '=' * (-len(s) % 4)
    return base64.urlsafe_b64decode(s.encode())

payload = json.loads(b64url(parts[1]))
print(json.dumps(payload, indent=2, sort_keys=True))
PY

위 코드 실행 전 TOKEN 환경변수로 토큰을 넣어주세요.

5단계: “공개 서비스”로 임시 전환해 레이어를 분리

원인이 IAM/토큰인지, 앱/네트워크인지 분리하려면 잠깐 공개로 바꿔보는 게 빠릅니다(운영 환경에서는 주의).

gcloud run services add-iam-policy-binding SERVICE_NAME \
  --region=REGION \
  --member="allUsers" \
  --role="roles/run.invoker"
  • 공개로 바꿨는데도 403이면, Cloud Run 앞단(게이트웨이/IAP/Armor) 또는 앱 로직 403일 가능성이 큽니다.
  • 공개로 바꾸면 200이 나오면, IAM/토큰 문제로 확정됩니다.

테스트 후 반드시 원복하세요.

gcloud run services remove-iam-policy-binding SERVICE_NAME \
  --region=REGION \
  --member="allUsers" \
  --role="roles/run.invoker"

6단계: 앞단 구성(API Gateway, IAP, LB, Armor)에서의 403 확인

Cloud Run을 직접 호출하는 게 아니라 다음을 붙이면 403의 출처가 바뀝니다.

  • API Gateway
  • Cloud Load Balancing(서버리스 NEG)
  • IAP
  • Cloud Armor

이 경우 Cloud Run 로그에 요청이 안 찍히는 패턴이 흔합니다.

체크 포인트

  • API Gateway를 쓰면, Gateway가 백엔드로 호출할 때 어떤 인증을 쓰는지 확인
  • IAP를 쓰면, 브라우저는 통과하는데 curl 은 403이 날 수 있음(쿠키/리다이렉트/헤더 차이)
  • Cloud Armor는 특정 IP/UA/경로를 막을 수 있음

게이트웨이 계층 디버깅은 “요청이 어디서 타임아웃/차단됐는지”를 계층별로 자르는 방식이 유효합니다. 비슷한 진단 습관은 EKS에서 Webhook 타임아웃? Admission 진단법과도 결이 같습니다.

7단계: 서비스 계정 체인(런타임 SA, 호출 SA, 권한 위임)을 정리

실제 운영에서는 Cloud Run을 사람이 직접 치는 게 아니라, 다른 워크로드가 호출합니다. 이때 403의 80%는 “Invoker를 잘못된 서비스 계정에 줌”에서 나옵니다.

대표 시나리오

  • Cloud Scheduler가 Cloud Run 호출
    • Scheduler job에 설정된 OIDC 서비스 계정에 roles/run.invoker 필요
  • Cloud Tasks가 Cloud Run 호출
    • Task가 발급하는 OIDC의 서비스 계정에 roles/run.invoker 필요
  • Cloud Run A가 Cloud Run B 호출
    • A의 런타임 서비스 계정에 B의 roles/run.invoker 필요

Cloud Run 서비스의 런타임 서비스 계정은 아래로 확인합니다.

gcloud run services describe SERVICE_NAME \
  --region=REGION \
  --format="value(spec.template.spec.serviceAccountName)"

호출하는 쪽(예: Cloud Run A)의 런타임 SA에, 호출 대상(Cloud Run B) invoker를 부여합니다.

gcloud run services add-iam-policy-binding TARGET_SERVICE \
  --region=REGION \
  --member="serviceAccount:CALLER_RUNTIME_SA@PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/run.invoker"

자주 나오는 403 케이스별 처방전

케이스 A: 브라우저는 되는데 curl 은 403

  • 브라우저는 로그인/IAP 쿠키로 통과
  • curl 은 토큰이 없거나 aud 불일치

해결:

  • Authorization: Bearer 로 ID 토큰 포함
  • --audiences 를 서비스 URL로 명시

케이스 B: 로컬에서는 되는데 Cloud Scheduler에서만 403

  • 내 사용자 계정에만 invoker를 줌
  • Scheduler job OIDC SA에는 invoker가 없음

해결:

  • Scheduler job에 설정된 SA에 roles/run.invoker 부여

케이스 C: 같은 프로젝트인데도 403

  • “프로젝트 레벨 권한”과 “서비스 IAM”을 혼동
  • 또는 리전이 다름

해결:

  • gcloud run services get-iam-policy 로 서비스 단 바인딩 확인
  • 명령에 --region 을 항상 고정

재현 가능한 최소 테스트 스크립트

아래 스크립트는 “토큰 포함 호출”과 “무토큰 호출”을 한 번에 비교해 403 원인을 좁히는 데 유용합니다.

set -euo pipefail

SERVICE_URL="https://YOUR_SERVICE-xxxxx.a.run.app"

echo "[1] no auth"
curl -sS -o /dev/null -w "status=%{http_code}\n" "$SERVICE_URL" || true

echo "[2] with identity token (aud explicit)"
TOKEN=$(gcloud auth print-identity-token --audiences="$SERVICE_URL")
curl -sS -o /dev/null -w "status=%{http_code}\n" \
  -H "Authorization: Bearer $TOKEN" \
  "$SERVICE_URL" || true
  • 1번이 403이고 2번이 200이면: 인증 필요 서비스가 정상이며, Invoker/IAM도 대체로 정상입니다.
  • 2번도 403이면: Invoker 바인딩(주체) 또는 토큰/audience 문제로 더 좁혀야 합니다.

마무리: 403은 “권한”이 아니라 “주체-권한 매칭” 문제다

Cloud Run 403은 단순히 roles/run.invoker 를 주면 끝나는 문제가 아니라,

  • 실제 호출 주체가 누구인지
  • 그 주체에 invoker가 붙어 있는지
  • 토큰의 aud 가 서비스 URL과 일치하는지
  • 앞단 게이트웨이에서 이미 막히는지

를 순서대로 분리해야 가장 빨리 해결됩니다.

운영에서 권한 이슈는 대개 한 번에 끝나지 않고, 인증 체인이 조금만 바뀌어도 재발합니다. 그래서 위 7단계를 문서화해 두면, 다음에 403이 떠도 “어디서부터 확인할지”가 자동으로 정해져 대응 속도가 크게 빨라집니다.