- Published on
GitHub Actions GITHUB_TOKEN 403 권한 오류 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
GitHub Actions를 붙이고 나면 가장 자주 마주치는 실패 중 하나가 GITHUB_TOKEN 관련 403 Forbidden입니다. 겉으로는 “토큰이 있는데 왜 403이지?”처럼 보이지만, 실제 원인은 대개 워크플로 권한 모델(permissions), 이벤트 트리거(pull_request vs pull_request_target), 포크 PR 보안 정책, 환경 보호 규칙, 조직/리포 설정 같은 “토큰이 발급되는 맥락”에 있습니다.
이 글에서는 403을 단순히 “권한을 더 주면 된다”로 끝내지 않고, 어떤 조건에서 어떤 권한이 필요한지, 그리고 그 권한을 안전하게 부여하는 방법을 단계적으로 정리합니다. (403 디버깅은 다른 환경에서도 비슷한 패턴으로 반복됩니다. 예를 들어 AWS에서 403을 파고들 때도 ‘정책/컨텍스트/토큰 발급 경로’를 같이 봐야 합니다. 비슷한 접근은 EKS Pod에서 SSM 세션 403 실패 원인과 해결 글에서도 확인할 수 있습니다.)
1) 먼저 403 메시지에서 “무엇이 거부됐는지” 확인
403이라도 원인이 여러 갈래입니다. 로그에서 아래 키워드를 먼저 찾으세요.
Resource not accessible by integrationRequires authentication(401에 가깝지만 상황에 따라 403처럼 보이기도 함)Insufficient permissions/permission denied to ...GitHub Actions is not permitted to create or approve pull requestsrefusing to allow a GitHub App to create or update workflow(워크플로 파일 수정 제한)
가장 흔한 케이스는 다음 두 가지입니다.
- 기본 권한이 Read로 떨어져서(또는 명시적으로 제한돼서)
contents: write,packages: write등이 없는 상태 - 포크 PR에서 실행된 워크플로라
GITHUB_TOKEN이 읽기 전용이거나, secrets가 주입되지 않는 상태
2) GitHub Actions 권한 모델 핵심: “기본값 + 오버라이드”
GITHUB_TOKEN은 워크플로 런마다 자동 발급되지만, 권한은 고정이 아닙니다.
- 리포지토리 설정(또는 조직 정책)의 Workflow permissions 기본값
- 워크플로 YAML의
permissions:블록 - 특정 Job 단위
permissions: - 이벤트가 포크 PR인지 여부
이 네 가지가 합쳐져 최종 권한이 결정됩니다.
2.1 리포지토리 기본 설정 확인
리포지토리 Settings → Actions → General → Workflow permissions
Read repository contents permission(읽기 전용)Read and write permissions(읽기/쓰기)
조직에서 강제로 Read-only로 고정하는 경우도 있습니다. 이때는 YAML에서 permissions: write-all을 줘도 기대대로 안 될 수 있으니, 조직 정책을 먼저 확인해야 합니다.
3) 가장 흔한 해결: permissions를 명시적으로 선언
403의 70%는 “권한이 없어서”입니다. 특히 actions/checkout 이후 git push, gh release, create tag, write package 등을 하면 바로 터집니다.
아래는 최소 권한 원칙으로, 필요한 권한만 명시하는 예시입니다.
name: release
on:
push:
tags:
- 'v*'
permissions:
contents: write # 태그/릴리즈 생성, git push
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "$GITHUB_REF_NAME" --generate-notes
3.1 PR 코멘트/리뷰/체크에 403이 날 때
PR에 코멘트를 달거나 체크 결과를 업데이트할 때는 다음 권한이 필요합니다.
- PR 코멘트:
pull-requests: write - 체크:
checks: write - 이슈 코멘트:
issues: write
permissions:
contents: read
pull-requests: write
checks: write
issues: write
4) “Resource not accessible by integration”의 정체
이 메시지는 단순히 권한 부족이 아니라, 현재 이벤트 컨텍스트에서 해당 리소스 접근이 원천적으로 제한될 때 자주 뜹니다.
대표 케이스:
pull_request이벤트가 포크에서 온 PR인 경우GITHUB_TOKEN이 읽기 전용으로 축소- secrets 미주입(기본 정책)
- 따라서 릴리즈 생성, 패키지 publish, PR에 쓰기 작업 등이 막힘
4.1 포크 PR에서 “안전하게” 쓰기 작업을 하고 싶다면
선택지는 크게 3개입니다.
- 쓰기 작업은 push/tag 등 신뢰된 이벤트에서만 수행 (권장)
- PR에서는 테스트만
- main merge 후 push에서 배포
pull_request_target사용
- 베이스 리포의 컨텍스트로 실행되어 secrets/쓰기 권한이 가능
- 대신 PR 코드(포크의 코드)를 그대로 checkout해서 실행하면 매우 위험
- GitHub App 토큰(또는 fine-grained PAT)로 제한된 권한 부여
GITHUB_TOKEN대신 별도 토큰을 사용- 권한 범위를 통제하기 쉬움
(권장 패턴) PR은 테스트만, 배포는 push에서
name: ci
on:
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
name: deploy
on:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
5) checkout 후 git push가 403일 때: 토큰/remote/브랜치 보호 점검
git push에서 403이 나는 경우는 크게 4가지입니다.
5.1 contents: write 누락
가장 먼저 permissions: contents: write를 확인하세요.
5.2 브랜치 보호 규칙(Protected Branch)
main이 보호되어 있고 “GitHub Actions가 푸시할 수 없음” 상태면 403이 납니다.
- Settings → Branches → 보호 규칙
- “Restrict who can push to matching branches”
- “Require pull request reviews…”
해결책:
- 직접 push 대신 PR 생성(자동 PR)로 전환
- 또는 GitHub App/봇 계정에 예외 부여
5.3 워크플로 파일 수정은 별도 제약
Actions가 .github/workflows/*.yml을 수정하는 커밋을 푸시하려 하면, 권한이 있어도 막히는 경우가 있습니다. 이는 공급망 공격 방지 정책과 관련이 있어, 설계 자체를 바꾸는 게 안전합니다.
5.4 remote URL이 잘못되어 다른 리포로 push
간혹 origin이 포크를 가리키거나, 권한 없는 리포로 push해서 403이 납니다.
git remote -v
6) GitHub API 호출이 403일 때: 필요한 스코프를 정확히 맞추기
curl/gh로 API를 때릴 때는 “엔드포인트가 요구하는 권한”이 다릅니다.
예: Packages 업로드(컨테이너/패키지)
permissions:
contents: read
packages: write
예: Actions 캐시/아티팩트/워크플로 관련
permissions:
actions: write
예: OIDC로 클라우드 AssumeRole을 할 때
permissions:
id-token: write
contents: read
OIDC 구성이 꼬이면 403/401/invalid token류가 섞여서 나타나는데, 이때도 “토큰 발급 컨텍스트 + 권한 + 신뢰정책”을 함께 봐야 합니다. (AWS 쪽 403/토큰 이슈를 파고드는 방식은 EKS Pod에서 STS 403 InvalidIdentityToken 해결에서의 접근과 유사합니다.)
7) 환경(Environment) 보호 규칙으로 인한 403/승인 대기
배포 Job이 environment:를 사용하고, 해당 환경에 Required reviewers나 deployment branch policy가 걸려 있으면 “권한 오류처럼 보이는 실패/대기”가 발생합니다.
jobs:
deploy:
environment: production
runs-on: ubuntu-latest
steps:
- run: echo deploy
이 경우는 토큰 권한 문제가 아니라, 환경 보호 규칙에 의해 “승인 전에는 진행 불가”인 것이므로:
- 환경의 Required reviewers 조정
- 배포 브랜치 정책에 main만 허용 등 조건 확인
을 점검해야 합니다.
8) 그래도 해결이 안 되면: 토큰을 바꾸는 전략(PAT vs GitHub App)
조직 정책상 GITHUB_TOKEN 권한을 올릴 수 없거나, 포크 PR에서도 제한된 쓰기 작업이 꼭 필요하면 대체 토큰을 고려합니다.
8.1 Fine-grained PAT
- 특정 리포/권한만 선택 가능
- 만료/회전 관리 필요
- Secrets에 저장 후 사용
- name: Push with PAT
env:
GH_PAT: ${{ secrets.GH_PAT }}
run: |
git config user.name "ci-bot"
git config user.email "ci-bot@example.com"
git remote set-url origin "https://x-access-token:${GH_PAT}@github.com/${GITHUB_REPOSITORY}.git"
git push origin HEAD:main
8.2 GitHub App 설치 토큰(권장)
- 권한을 앱 단위로 최소화 가능
- 토큰이 단기(expiring)라 운영에 유리
- 조직 환경에서 표준화하기 좋음
(구현은 앱 생성/설치/키 관리가 필요하므로 여기서는 개념만 다루되, “권한을 코드가 아니라 신원(앱)으로 분리한다”는 점이 핵심입니다.)
9) 실전 디버깅 체크리스트(빠르게 원인 좁히기)
- 이벤트가 무엇인가?
push/workflow_dispatch면 비교적 자유로움pull_request+ 포크면 쓰기/시크릿이 제한됨
- 로그에
Resource not accessible by integration가 있는가?- 포크/컨텍스트 제한 의심
- 워크플로에
permissions:가 명시되어 있는가?- 최소 권한으로 필요한 항목을 추가했는가?
- 리포 설정에서 Workflow permissions가 Read-only로 고정인가?
- 브랜치 보호 규칙이 GitHub Actions의 push를 막고 있는가?
- Environment 보호 규칙으로 승인 대기/차단이 걸렸는가?
- 정말
GITHUB_TOKEN으로 해야 하는가?- GitHub App/PAT로 분리하는 편이 안전한가?
결론
GITHUB_TOKEN 403은 “토큰이 틀렸다”기보다 “토큰이 발급된 컨텍스트에서 허용되지 않는 작업을 했다”는 신호인 경우가 대부분입니다. 따라서 해결의 핵심은 (1) 필요한 작업을 신뢰된 이벤트로 옮기고, (2) permissions를 최소 권한으로 정확히 선언하며, (3) 포크 PR/보호 규칙/환경 승인 같은 정책 레이어를 함께 점검하는 것입니다.
403을 만날 때마다 권한을 무작정 키우기보다, 어떤 리소스에 어떤 경로로 접근하는지부터 분해해 보면 재발도 크게 줄일 수 있습니다. (비슷한 403 트러블슈팅 사고방식은 EKS에서 Pod는 되는데 SQS만 403 뜰 때 같은 케이스에서도 그대로 적용됩니다.)