[GitHub Actions][OIDC] id-token: write가 없을 때 토큰 요청이 실패하는 이유
IT 리서치 노트
[GitHub Actions][OIDC] id-token: write가 없을 때 토큰 요청이 실패하는 이유
GitHub Actions에서 OIDC를 붙였는데도 클라우드 로그인이나 Trusted Publishing 단계가 바로 실패한다면, 가장 먼저 볼 값은 permissions의 id-token: write다. GitHub 공식 문서 기준으로 이 권한이 없으면 OIDC JWT를 아예 요청할 수 없고, permissions를 일부만 적었을 때 나머지 권한이 none으로 떨어지는 점까지 겹치면 문제를 더 오래 끈다. 이 글은 어느 job에만 OIDC 권한을 줘야 하는지, reusable workflow에서는 어디에 선언해야 하는지, 토큰 요청 실패를 어떤 순서로 좁힐지 실무 기준으로 정리한다.
1. 개요
OIDC 문제를 만났을 때 많은 팀이 먼저 클라우드 IAM이나 provider action 버전을 의심한다. 그런데 GitHub 문서에서 더 앞에 있는 전제는 단순하다. OIDC 토큰을 만들 job 또는 workflow에 id-token: write가 있어야 하고, 그 권한은 외부 리소스에 쓰기 권한을 주는 것이 아니라 JWT를 요청할 수 있게 하는 토글에 가깝다.
실무에서 오래 막히는 이유는 이 전제가 YAML 전체에 흩어져 있기 때문이다. top-level permissions를 일부만 적어 나머지를 none으로 만들어 버리거나, reusable workflow를 호출하면서 caller 쪽 선언을 빠뜨리거나, build job과 deploy job을 분리해 놓고 OIDC 권한을 잘못된 job에 주는 식이다.
id-token: write가 빠지면 GitHub OIDC provider가 JWT를 만들지 못한다.- permissions를 하나라도 명시하면 적지 않은 권한은
none으로 바뀔 수 있다. - caller와 reusable workflow의 경계, environment 조건, cloud trust policy를 따로 읽어야 재발을 막을 수 있다.
2. 왜 여기서 막히는가
겉으로 보이는 증상은 비슷하지만 실제 원인은 몇 갈래로 갈린다. AWS, Azure, GCP 로그인 action이나 npm Trusted Publishing 단계가 실패해도, 먼저 실패한 지점이 cloud provider 쪽인지 GitHub가 JWT를 주기 전 단계인지 분리하지 않으면 권한을 잘못 늘리기 쉽다.
- deploy job에만 OIDC가 필요한데 workflow 전체 권한을 줄이면서
id-token항목을 빼먹는다. contents: read만 남기고 배포 job에 필요한 OIDC 권한을 적지 않아 JWT 요청 자체가 실패한다.- reusable workflow를 호출하면서 caller 쪽
permissions선언을 빠뜨려 외부 workflow에서 토큰이 보이지 않는다. - permissions를 일부만 적은 뒤, 적지 않은 scope가
none으로 바뀐 사실을 모르고 provider 설정만 반복 수정한다. - fork PR, environment, branch 조건까지 한 번에 바꿔 어떤 실패가 먼저 발생했는지 로그를 구분하지 못한다.
GitHub workflow syntax 문서에서 놓치기 쉬운 문장이 하나 있다. permissions에서 일부 권한만 지정하면, 지정하지 않은 권한은 자동으로 남아 있는 것이 아니라 none으로 처리될 수 있다. 그래서 기존에 동작하던 배포 workflow도 권한 최소화를 하겠다고 YAML을 정리한 직후 갑자기 OIDC 단계에서 실패할 수 있다.
또 하나는 reusable workflow다. 같은 조직이나 엔터프라이즈 내부에서 재사용할 때와, 외부 workflow를 호출할 때의 선언 위치가 다르다. GitHub OIDC reference는 외부 reusable workflow에서는 caller workflow나 caller job에 id-token: write를 명시하라고 적고 있다. 배포 공통화를 해 놓은 팀에서 여기서 많이 막힌다.
문제정의 단계에서 해야 할 일은 단순하다. 첫 번째 실패 로그가 JWT를 받기 전인지, 받은 뒤 provider token 교환에서 실패했는지, 아니면 environment나 branch 조건 때문에 토큰은 받아도 클라우드가 거절했는지를 순서대로 나누는 것이다. 이 선을 못 그으면 보안은 약해지고 장애는 길어진다.
3. 토큰 요청 실패를 좁히는 순서
OIDC 배포는 build와 deploy를 같은 권한으로 묶지 않는 편이 안전하다. 아래 순서는 실제 YAML과 로그를 같이 보면서 어디서 실패했는지 좁힐 때 쓰기 좋다.
- 먼저 OIDC가 필요한 job이 정확히 어디인지 적는다. 테스트, lint, artifact build job에는 보통 필요 없다.
- 그 job 또는 workflow에
permissions를 열고id-token: write와 필요한 최소한의contents: read만 남긴다. - permissions를 top-level에 둘지, deploy job 안에만 둘지 결정한다. OIDC가 한 job에서만 필요하면 job 단위가 더 안전하다.
- reusable workflow를 쓴다면 caller 쪽과 called workflow 쪽 선언 위치를 따로 확인한다. 특히 조직 밖 reusable workflow는 caller 선언 누락이 흔하다.
- 그 다음에야 cloud trust policy, subject claim, branch, environment 조건을 본다. 여기까지 와야 provider 쪽 문제를 보는 순서가 맞다.
이 예제에서 중요한 점은 deploy job만 OIDC 권한을 가진다는 것이다. build job은 checkout과 테스트만 하므로 contents: read로 끝내고, 배포 job이 JWT를 요청하는 순간에만 id-token: write를 연다. 이렇게 나누면 실패 지점도 로그에서 바로 구분된다.
권한 최소화 일반론이 필요하면 GITHUB_TOKEN 권한과 OIDC를 workflow별로 줄이는 방법을 같이 보면 좋다. 그리고 OIDC가 실제 패키지 배포에서 어떻게 쓰이는지는 npm Trusted Publishing으로 npm token 없이 배포하는 절차와 연결해서 보면 맥락이 더 빨리 잡힌다. provider trust policy에서 GitHub 쪽 조건을 더 좁히려면 subject claim과 environment 조건을 같이 묶어 배포 범위를 줄이는 법까지 이어서 보는 편이 좋다.
실패 로그를 읽을 때도 순서를 바꾸지 않는 편이 낫다. OIDC 토큰 요청이 되지 않았는데도 클라우드 IAM 조건부터 바꾸면 배포는 여전히 실패하고 권한만 넓어진다. 먼저 GitHub가 JWT를 만들 수 있는지 확인하고, 그 다음에 subject claim과 branch 또는 environment 규칙을 본다.
4. 문서 화면으로 확인할 항목
아래 화면에서는 OIDC reference, workflow syntax, cloud provider 가이드를 같은 순서로 본다. 토큰이 발급되는 조건, YAML 선언 위치, reusable workflow 예외, 최소 권한 원칙을 각각 다른 화면에서 확인해야 실제 배포 실패를 빠르게 좁힐 수 있다.
먼저 OIDC reference에서 Required permission 항목을 본다. GitHub 문서가 가장 먼저 못 박는 조건은 이 권한이 있어야 OIDC provider가 JWT를 만들 수 있다는 점이다.
같은 문서의 Setting permissions 구간은 선언 위치를 나눠 볼 때 중요하다. workflow 전체에 둘지, 특정 job 안에만 둘지 예제 코드가 바로 이어져 있어서 실수한 YAML을 비교하기 좋다.
cloud provider 가이드에서는 OIDC 적용 순서를 더 짧게 보여 준다. GitHub 문서는 먼저 permissions를 추가하고, 그 다음 provider action으로 JWT를 access token으로 교환하라고 순서를 분리한다.
reusable workflow를 쓰는 팀은 이 구간을 꼭 본다. 특히 조직 밖 reusable workflow에서는 caller 쪽에 id-token 권한을 명시해야 한다는 조건이 운영 장애로 자주 이어진다.
마지막으로 workflow syntax의 permissions 설명을 본다. 여기서 일부 권한만 지정하면 나머지가 none이 된다는 규칙을 놓치면, 기존 배포 job이 갑자기 OIDC 단계에서 실패해도 원인을 잘못 짚기 쉽다.
이 순서로 확인하면 JWT를 발급받기 전 문제인지, permissions 선언 위치 문제인지, provider 쪽 trust policy 문제인지 경계가 선다. 문서 한 장만 보고 권한을 넓히기보다, 각 화면에서 어떤 조건이 앞단인지 뒤단인지 먼저 정리하는 편이 안전하다.
5. 권한을 넓히기 전에 볼 리스크
토큰 요청 실패를 빨리 끝내고 싶어서 workflow 전체에 넓은 권한을 주면 단기적으로는 편해 보여도 나중에 더 큰 문제를 만든다. GitHub secure use 문서도 기본값은 최소 권한으로 두고 job 단위로 늘리라고 권장한다.
id-token: write를 workflow 전체에 주면 OIDC가 필요 없는 step도 같은 토큰 요청 능력을 갖게 된다.contents: write를 같이 넓게 열어 두면 배포와 무관한 action이 저장소를 수정할 여지도 생긴다.- cloud trust policy에서 subject 조건을 느슨하게 두면 다른 branch나 다른 workflow도 같은 role을 받을 수 있다.
- environment 보호 규칙 없이 OIDC만 붙이면 JWT는 발급돼도 배포 통제가 약해질 수 있다.
특히 fork에서 올라오는 PR과 main branch 배포를 같은 권한 모델로 다루면 위험하다. 테스트 job은 읽기 전용으로 두고, 실제 배포 job은 branch와 environment 조건이 맞을 때만 OIDC를 요청하게 나누는 편이 낫다. 권한 문제를 해결할 때는 항상 어떤 job이 어떤 외부 리소스를 요청하는지 문장으로 적어 두는 편이 유지보수에 도움이 된다.
6. 결론
OIDC 배포가 실패했을 때 첫 체크포인트는 cloud provider보다 GitHub 쪽 permissions다.
- JWT가 필요한 job에만
id-token: write를 준다. - permissions를 일부만 적었다면 나머지가
none이 되는지 다시 본다. - reusable workflow와 environment 조건은 caller, callee, provider 정책을 나눠 확인한다.
핵심은 추상적으로 보안을 강화하는 것이 아니라, 어떤 job이 JWT를 요청하고 어떤 job은 절대 요청하지 않도록 설계하는 것이다. 이 선만 명확해도 OIDC 토큰 요청 실패는 로그 해석이 훨씬 빨라지고, 권한을 필요 이상으로 넓히는 실수도 줄어든다.
7. 참고 링크
- https://docs.github.com/en/actions/reference/security/oidc
- https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
- https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-cloud-providers
- https://docs.github.com/en/actions/reference/security/secure-use
- https://docs.github.com/en/actions/concepts/security/github_token