Published on

GitHub Actions GITHUB_TOKEN 403 권한오류 해결

Authors

서론

GitHub Actions를 돌리다 보면 가장 당황스러운 실패 중 하나가 403입니다. 로그에는 보통 Resource not accessible by integration, Permission denied to github-actions[bot], 403: Write access to repository not granted 같은 문구가 섞여 나옵니다. 로컬에서는 잘 되는데 CI에서만 실패한다면, 대부분은 네트워크 문제가 아니라 권한 모델 문제입니다.

이 글에서는 GITHUB_TOKEN이 어떤 토큰인지부터, 어떤 상황에서 403이 나는지(특히 PR, 포크, 환경 보호 규칙, 조직 정책), 그리고 가장 빠르게 고칠 수 있는 설정 패턴을 단계별로 정리합니다.

> 참고: AWS 배포에서 OIDC 토큰이 거부되는 케이스도 “권한/신뢰 정책” 문제라는 점에서 결이 비슷합니다. 필요하면 GitHub Actions OIDC AWS 배포 InvalidIdentityToken 해결도 함께 보세요.

1) GITHUB_TOKEN의 정체: “런타임에 발급되는 제한 토큰”

GITHUB_TOKEN은 워크플로 실행 시점에 GitHub가 자동 발급해 주는 리포지토리 스코프 토큰입니다.

  • 기본 용도: 체크아웃, GitHub API 호출, 릴리즈 생성, 패키지 퍼블리시 등
  • 장점: 시크릿 저장 불필요, 자동 로테이션
  • 핵심 제약:
    • 기본 권한이 제한적(특히 “Write”가 막혀 있는 경우가 많음)
    • 이벤트 컨텍스트(PR from fork 등)에 따라 권한이 강제로 더 축소
    • 리포지토리/조직 보안 정책에 의해 상한이 정해짐

즉, 403은 대개 “토큰이 없어서”가 아니라 “토큰은 있는데 그 작업을 할 권한이 없어서” 발생합니다.

2) 403 에러 메시지로 원인 분류하기

2.1 Resource not accessible by integration

가장 흔합니다. 보통 아래 중 하나입니다.

  • PR이 **포크(fork)**에서 올라온 경우
  • 워크플로가 pull_request 이벤트로 실행되며, 그 안에서 write 작업(커밋 푸시, 라벨 추가, 릴리즈 생성 등)을 시도
  • 조직/리포에서 GITHUB_TOKEN 권한이 Read-only로 제한

2.2 Write access to repository not granted

  • permissionscontents: read로 되어 있거나, 기본값이 read-only
  • actions/checkout가 기본 설정으로 체크아웃되었지만, push 단계에서 권한 부족

2.3 Git push 단계에서 remote: Permission to ... denied to github-actions[bot]

  • 토큰 권한 부족
  • 혹은 checkout이 PR merge ref를 잡고 있어서 push 대상 브랜치와 맞지 않음
  • 브랜치 보호 규칙이 “Actions의 push 금지” 형태로 되어 있음

3) 가장 먼저 확인할 것: 리포 설정의 Workflow permissions

리포지토리에서 다음 경로를 확인합니다.

  • SettingsActionsGeneralWorkflow permissions

여기서:

  • Read and write permissions 로 설정되어 있는지
  • 필요 시 Allow GitHub Actions to create and approve pull requests가 켜져 있는지

조직(Organization) 레벨에서 더 강하게 강제하는 경우도 있으니, 조직 설정도 함께 확인해야 합니다.

4) 워크플로에서 permissions를 명시하라 (가장 실전적인 해결)

GitHub는 보안 강화를 위해 GITHUB_TOKEN 기본 권한을 점점 더 보수적으로 가져가는 편입니다. 따라서 워크플로 파일에서 필요 권한만 최소로 명시하는 게 안정적입니다.

4.1 릴리즈/태그/푸시가 필요한 경우 예시

name: release

on:
  push:
    branches: ["main"]

permissions:
  contents: write   # 태그/릴리즈/커밋 푸시
  packages: write   # ghcr 등 패키지 퍼블리시 시

jobs:
  build-and-release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Create tag
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git tag -a "v1.0.0" -m "release v1.0.0"
          git push origin "v1.0.0"

포인트:

  • permissions: contents: write가 없으면 push/태그 생성에서 403이 날 확률이 큽니다.
  • fetch-depth: 0은 태그/히스토리 기반 작업에서 자주 필요합니다(권한과는 별개지만 릴리즈 파이프라인에서 함께 터지기 쉬움).

4.2 PR에 라벨/코멘트 달기, 체크 런 업데이트 등

permissions:
  pull-requests: write
  issues: write
  checks: write

contents: write만으로는 PR 관련 API가 막히는 경우가 있습니다. 필요한 도메인 권한을 분리해서 주는 게 좋습니다.

5) PR from fork에서 write 작업은 원칙적으로 막힌다

보안상 포크 PR은 악성 코드가 워크플로를 수정해 시크릿/권한을 탈취할 수 있기 때문에, GitHub는 다음을 강하게 제한합니다.

  • pull_request 이벤트에서 포크 PR은 GITHUB_TOKEN이 read-only로 축소되는 경우가 많음
  • 시크릿도 기본적으로 제공되지 않음

5.1 해결 패턴 A: pull_request_target 사용(주의 필요)

pull_request_target베이스 리포지토리 컨텍스트로 실행되므로 write 권한을 확보할 수 있습니다. 하지만 체크아웃/실행 대상을 잘못 다루면 위험합니다.

안전한 원칙:

  • PR의 “코드”를 그대로 실행하지 말 것
  • 필요한 경우에도 최소 권한 부여

예시(라벨링 같은 메타 작업만 수행):

on:
  pull_request_target:
    types: [opened, synchronize]

permissions:
  pull-requests: write

jobs:
  label:
    runs-on: ubuntu-latest
    steps:
      - name: Add label
        run: |
          gh pr edit "$PR_URL" --add-label "needs-review"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_URL: ${{ github.event.pull_request.html_url }}

5.2 해결 패턴 B: 포크 PR에서는 read-only로만 검증하고, 머지 후에 배포/릴리즈

  • PR에서는 테스트/빌드만 수행
  • push(main)에서 릴리즈/배포 수행

대부분의 팀에서 가장 안전하고 운영이 단순합니다.

6) 브랜치 보호 규칙(Branch protection) 때문에 403이 나는 경우

contents: write를 줬는데도 push가 막히면, 다음을 확인합니다.

  • SettingsBranches → Branch protection rules
    • “Require pull request reviews”
    • “Restrict who can push to matching branches”
    • “Require status checks”

특히 Restrict who can push가 켜져 있으면 github-actions[bot]이 푸시할 수 없어서 403/denied가 납니다.

해결책:

  • Actions가 직접 main에 push해야 한다면(버전 범프 등) “Allow specified actors”에 GitHub Actions를 포함하거나,
  • 더 권장되는 방식으로는 Actions가 직접 push하지 않고 PR을 생성하게 바꾸는 것입니다.

7) checkout 설정 실수로 “다른 ref”에 push하려는 경우

PR 워크플로에서 아래처럼 체크아웃하면 실제로는 refs/pull/.../merge 같은 가상 ref를 잡습니다.

- uses: actions/checkout@v4

이 상태에서 git push origin HEAD:main 같은 작업을 하면 권한/정책/refs 문제로 실패할 수 있습니다.

해결:

  • PR 코드를 push할 목적이라면(대개 권장되지 않음) 정확한 브랜치를 체크아웃해야 합니다.
- uses: actions/checkout@v4
  with:
    ref: ${{ github.head_ref }}

다만 포크 PR에서는 어차피 write가 막힐 가능성이 크니, 설계를 바꾸는 편이 낫습니다.

8) 그래도 안 되면: PAT vs GitHub App 중 무엇을 쓸까?

8.1 PAT(개인 액세스 토큰)

장점:

  • 가장 단순하게 “권한 문제”를 우회 가능

단점:

  • 개인 계정에 종속
  • 권한 과다 부여 위험
  • 만료/회수 관리 필요

사용 예:

- name: Push with PAT
  run: git push origin HEAD:main
  env:
    GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}

(실제로는 remote URL에 토큰을 넣거나 actions/checkouttoken:을 넣는 방식이 더 일반적입니다.)

8.2 GitHub App(권장)

  • 조직/리포 단위로 설치
  • 필요한 권한만 부여 가능
  • 토큰이 단기 발급되어 운영/보안이 좋음

대규모 조직이나 장기 운영 파이프라인이라면 GitHub App이 더 안전합니다.

9) 빠른 체크리스트(문제 재발 방지)

  • 리포/조직 Workflow permissions가 read-only로 강제되어 있지 않은가?
  • 워크플로 상단에 permissions:를 명시했는가?
  • PR from fork인데 write 작업을 하고 있지 않은가?
  • pull_request에서 배포/릴리즈/태그 생성 같은 write 작업을 하지 않는가?
  • 브랜치 보호 규칙이 Actions의 push를 막고 있지 않은가?
  • actions/checkout이 올바른 ref를 체크아웃했는가?
  • 필요한 경우 PAT/GitHub App로 권한 모델을 분리했는가?

10) 결론: 403은 “토큰 문제”가 아니라 “권한 설계 문제”다

GITHUB_TOKEN 403은 대개 다음 두 줄로 정리됩니다.

  1. 권한을 명시하라: permissions: contents: write 등 필요한 최소 권한을 워크플로에 선언
  2. 이벤트 컨텍스트를 분리하라: PR(특히 포크)에서는 read-only 검증, write 작업(릴리즈/배포)은 main push에서 수행

이 두 가지를 적용하면 Resource not accessible by integration 류의 403은 대부분 사라집니다. 이후에도 막힌다면 브랜치 보호 규칙과 조직 정책을 확인하고, 장기적으로는 GitHub App 기반 권한 분리를 고려하는 것이 가장 안정적입니다.