- Published on
GitHub Actions 403 권한 오류 해결 - GITHUB_TOKEN
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
GitHub Actions를 붙여 CI/CD를 돌리다 보면 어느 순간부터 403이 터지면서 배포, 태그 푸시, 릴리스 생성, PR 코멘트 작성 같은 작업이 막히는 경우가 많습니다. 로그를 보면 대개 Resource not accessible by integration, Permission denied to ..., HttpError: 403 같은 메시지가 섞여 나오고, 원인은 대부분 GITHUB_TOKEN의 권한이 기대와 다르거나 이벤트 컨텍스트가 제한된 상태에서 쓰였기 때문입니다.
이 글에서는 GITHUB_TOKEN이 정확히 무엇인지, 왜 같은 워크플로가 어떤 브랜치/이벤트에서는 되고 어떤 경우엔 403이 나는지, 그리고 실무에서 가장 안전하게 고치는 패턴(권한 최소화, PAT 대체, OIDC 전환)을 단계별로 정리합니다.
403이 나는 대표 시나리오 5가지
1) 기본 권한이 read로 제한됨
조직/레포 설정 또는 GitHub의 기본 정책에 의해 워크플로의 GITHUB_TOKEN 권한이 읽기 전용으로 제한되면, git push, 릴리스 생성, 이슈/PR 코멘트 작성 등 쓰기 작업에서 403이 납니다.
특히 최근에는 보안 강화를 위해 신규 레포에서 기본값이 read인 경우가 늘었습니다.
2) pull_request(fork 포함)에서 토큰이 강하게 제한됨
포크에서 올라온 PR을 pull_request 이벤트로 실행하면, GITHUB_TOKEN은 민감한 권한(쓰기, 시크릿 접근 등)이 제한됩니다. 이때 API 호출이나 푸시를 시도하면 Resource not accessible by integration 형태로 403이 납니다.
여기서 중요한 포인트는 “워크플로 파일에 permissions를 적어도” 이벤트 컨텍스트 자체가 제한이면 뚫리지 않는다는 점입니다.
3) pull_request_target을 잘못 써서 반대로 막히거나 위험해짐
pull_request_target은 베이스 브랜치 컨텍스트로 실행되기 때문에 토큰 권한이 상대적으로 높고 시크릿도 접근 가능합니다. 그래서 403은 줄어들 수 있지만, PR에서 넘어온 코드를 그대로 실행하면 공급망 공격이 됩니다.
즉 403을 피하려고 무턱대고 pull_request_target로 바꾸면 보안 사고로 이어질 수 있습니다.
4) GitHub API 엔드포인트에 필요한 스코프가 부족함
예를 들어 릴리스 생성, 환경 보호 규칙이 걸린 배포, 패키지 푸시, Pages 배포 등은 각각 필요한 권한이 다릅니다. contents: write만으로는 부족한 경우가 있고, packages: write, pull-requests: write, issues: write 등이 추가로 필요할 수 있습니다.
5) 브랜치 보호/태그 보호/환경 보호 규칙에 의해 거부됨
GITHUB_TOKEN 권한이 충분해도, 브랜치 보호 규칙(직접 푸시 금지), 태그 보호, environment 승인 요구 등이 걸려 있으면 결과적으로 푸시나 배포가 403 또는 유사 오류로 실패합니다.
먼저 확인할 체크리스트
워크플로가 어떤 이벤트로 실행되는가
push/workflow_dispatch: 비교적 단순pull_request: 포크 PR이면 강한 제한pull_request_target: 권한은 높지만 보안 위험
레포 설정에서 Actions 권한 기본값
레포 Settings의 Actions 관련 설정에서 워크플로 토큰 권한이 Read repository contents permission 같은 형태로 제한될 수 있습니다. 조직 정책으로 강제되는 경우도 있습니다.
로그에서 힌트 문구
Resource not accessible by integration: 이벤트 컨텍스트/토큰 권한 제한 가능성이 큼Permission to ... denied to github-actions[bot]: 권한 또는 보호 규칙 문제 가능성이 큼Must have admin rights to Repository: 관리자 권한이 필요한 API 호출
해결 1) 워크플로에 permissions를 명시해서 권한을 올바르게 부여
가장 먼저 할 일은 워크플로 상단에 필요한 최소 권한을 명시하는 것입니다. 명시하지 않으면 기본값(조직/레포 정책)을 따르며, 이게 의외로 read로 떨어져 403이 납니다.
아래 예시는 태그 푸시/릴리스 생성 등 contents 쓰기가 필요한 경우입니다.
name: release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
create-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
핵심은 permissions를 “필요한 것만” 정확히 적는 것입니다. 예를 들어 PR 코멘트를 달려면 다음이 필요합니다.
permissions:
pull-requests: write
이슈에 코멘트를 달면:
permissions:
issues: write
패키지 레지스트리에 푸시하면:
permissions:
packages: write
해결 2) actions/checkout에 persist-credentials와 푸시 권한을 함께 점검
git push가 403으로 실패하는 경우, 토큰 권한뿐 아니라 체크아웃 단계에서 자격 증명 설정이 꼬인 경우도 있습니다.
기본적으로 actions/checkout은 토큰을 git remote에 주입합니다. 하지만 보안/커스터마이징 때문에 persist-credentials: false로 꺼두면 이후 git push가 인증 없이 나가서 실패합니다.
- uses: actions/checkout@v4
with:
persist-credentials: true
fetch-depth: 0
- name: Push tag
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag "v1.2.3"
git push origin "v1.2.3"
그리고 이 경우에도 워크플로 상단에 다음이 필요합니다.
permissions:
contents: write
해결 3) 포크 PR에서는 “쓰기 작업을 하지 않는 구조”로 분리
포크 PR에서 403을 피하는 가장 안전한 방법은, PR 검증 워크플로에서는 빌드/테스트만 하고 “레포에 쓰기”가 필요한 작업(코멘트 작성, 라벨링, 자동 머지, 릴리스, 배포)은 push 또는 workflow_dispatch로 분리하는 것입니다.
예를 들어:
pull_request: lint/test/build만 수행pushtomain: 배포/릴리스/태그 푸시
name: pr-check
on:
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
이렇게 하면 포크 PR 제한 때문에 403이 나는 영역을 원천 차단할 수 있고, 보안 모델도 깔끔해집니다.
해결 4) 정말 필요할 때만 PAT로 대체 (단, 최소 권한)
어쩔 수 없이 포크 PR에서도 특정 작업(예: 코멘트, 라벨링)을 해야 한다면 PAT를 쓰는 경우가 있습니다. 다만 PAT는 유출 시 피해가 크므로 권장 패턴은 다음과 같습니다.
- Fine-grained PAT 사용
- 최소 권한만 부여
- 가능한 경우 조직 정책과 승인 프로세스 적용
예시:
- name: Comment on PR
env:
GH_TOKEN: ${{ secrets.BOT_PAT }}
run: |
gh pr comment "${{ github.event.pull_request.number }}" \
--body "CI 결과를 확인해주세요."
주의할 점:
- 포크 PR에서 시크릿은 기본적으로 주입되지 않습니다. 따라서 이 방식은 “포크 PR에서 실행되는 워크플로”에는 그대로 적용되지 않을 수 있습니다.
- 이 때문에 실무에서는 “쓰기 작업은 베이스 레포에서 별도 워크플로로 수행”하는 구조가 더 안전합니다.
해결 5) 클라우드 배포는 OIDC로 전환해 GITHUB_TOKEN 의존도를 줄이기
403을 해결하려다 보면 결국 GITHUB_TOKEN으로 클라우드 자격 증명까지 해결하려고 시도하는 경우가 있는데, 이건 영역이 다릅니다. GitHub API 권한과 클라우드 IAM 권한은 별개고, 장기적으로는 OIDC가 정석입니다.
예를 들어 AWS 배포에서 OIDC를 쓰면:
- 레포 쓰기 권한 문제와 분리
- 장기 키(PAT/Access Key) 저장을 줄임
- 감사/회전이 쉬움
OIDC 설정에서 자주 만나는 인증 오류 흐름은 EKS 쪽 이슈와도 결이 비슷합니다. OIDC 기반 인증 자체가 막히는 케이스는 다음 글도 함께 참고하면 좋습니다.
자주 터지는 케이스별 처방전
케이스 A: 릴리스 생성이 403
- 워크플로에
permissions: contents: write추가 - 태그가 보호되어 있지 않은지 확인
gh release create는GH_TOKEN이 필요하니 환경변수 지정 확인
케이스 B: PR에 코멘트/라벨링이 403
permissions: pull-requests: write또는issues: write추가- 이벤트가 포크 PR이면 구조를 분리(권장)
케이스 C: git push가 403
permissions: contents: writeactions/checkout의persist-credentials확인- 브랜치 보호 규칙으로 직접 푸시가 막혀 있으면 PR 기반으로 변경
케이스 D: 특정 브랜치에서만 실패
- 브랜치 보호 규칙 차이
- 환경 보호 규칙(승인 필요) 차이
CODEOWNERS/필수 체크 설정 차이
디버깅 팁: 현재 토큰 권한과 호출 주체를 로그로 확인
API 호출 주체가 github-actions[bot]인지, 어떤 권한으로 호출되는지 확인하면 원인 파악이 빨라집니다.
- name: Who am I (GitHub API)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api user
권한이 부족하면 여기서도 403이 나거나, 이후 엔드포인트에서 403이 재현됩니다. 또한 이벤트 컨텍스트(pull_request인지 push인지)에 따라 결과가 달라지는지 비교하면 문제 범위를 빠르게 좁힐 수 있습니다.
보안 관점에서의 권장 결론
- 워크플로에는 항상
permissions를 명시하고 최소 권한만 부여합니다. - 포크 PR 컨텍스트에서는 쓰기 작업을 하지 않도록 파이프라인을 분리합니다.
- 정말 필요한 경우에만 PAT를 쓰되, Fine-grained와 최소 권한을 지키고 수명/회전 정책을 둡니다.
- 클라우드 배포 인증은
GITHUB_TOKEN로 억지로 해결하지 말고 OIDC로 분리합니다.
운영 환경에서 403은 단순히 “권한을 올리면 해결”되는 문제가 아니라, 이벤트 컨텍스트/보호 규칙/보안 모델을 함께 맞춰야 재발이 줄어듭니다. 재시도나 백오프 같은 안정화 전략이 필요한 외부 API 호출이 섞인 파이프라인이라면, 실패 패턴을 분류하고 복구 전략을 설계하는 접근도 도움이 됩니다.