- Published on
GCP Cloud Run 403 해결 - IAM·Invoker 권한 7단계
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
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가 있어야 합니다. - 인증 없이 허용(공개):
allUsers에roles/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필요
- Scheduler job에 설정된 OIDC 서비스 계정에
- Cloud Tasks가 Cloud Run 호출
- Task가 발급하는 OIDC의 서비스 계정에
roles/run.invoker필요
- Task가 발급하는 OIDC의 서비스 계정에
- Cloud Run A가 Cloud Run B 호출
- A의 런타임 서비스 계정에 B의
roles/run.invoker필요
- A의 런타임 서비스 계정에 B의
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이 떠도 “어디서부터 확인할지”가 자동으로 정해져 대응 속도가 크게 빨라집니다.