Published on

GitHub Actions 권한 에러 해결 - GITHUB_TOKEN·OIDC

Authors

서버나 클라우드 배포 자동화를 GitHub Actions로 구성하다 보면, 워크플로가 갑자기 403 또는 permission denied로 실패하는 순간이 자주 옵니다. 특히 GITHUB_TOKEN 기본 권한이 바뀌었거나, PR 컨텍스트(포크 PR)에서 토큰 권한이 제한되거나, 클라우드 인증을 OIDC로 바꾸는 과정에서 id-token 권한을 빠뜨리면 동일한 증상이 반복됩니다.

이 글에서는 GitHub Actions의 권한 모델을 GITHUB_TOKEN과 OIDC 두 축으로 나눠서, 실제로 많이 만나는 에러 메시지별 원인과 해결책을 코드 중심으로 정리합니다. (원칙은 간단합니다: 권한을 “추측”하지 말고, 최소 권한으로 “명시”하고, 토큰/클레임/정책을 “검증”하세요.)

관련해서 권한/인증 문제를 빠르게 진단하는 방법론은 S3 403 진단 글과도 결이 비슷합니다. 필요하면 함께 참고하세요: AWS S3 403 AccessDenied - 버킷정책·SCP 10분 진단

1) GitHub Actions 권한 모델 빠르게 정리

GITHUB_TOKEN이란

GITHUB_TOKEN은 워크플로 실행 시 GitHub가 자동 발급하는 설치 토큰입니다. 보안상 다음 특성이 있습니다.

  • 리포지토리/조직 설정에 따라 기본 권한이 제한될 수 있음
  • PR 이벤트에서 특히 제한이 강함(포크 PR은 더 강함)
  • permissions 키로 토큰 스코프를 명시적으로 줄이거나 늘릴 수 있음

즉, 예전에는 되던 git push나 릴리스 생성이 어느 날부터 막히는 경우가 흔합니다. 이때는 “토큰이 없다”가 아니라 “토큰이 있는데 권한이 부족”한 상태입니다.

OIDC란

OIDC는 GitHub Actions가 외부 클라우드(AWS/Azure/GCP 등)에 대해 “단기 토큰”을 발급받도록 해주는 페더레이션 방식입니다.

  • 장점: 장기 시크릿(Access key, Service principal secret)을 저장하지 않아도 됨
  • 핵심 요구사항: 워크플로에 permissions: id-token: write가 필요
  • 추가 요구사항: 클라우드 측 신뢰 정책(issuer, audience, subject, repo, ref 등)이 정확해야 함

2) 가장 흔한 에러와 의미

에러 A: Resource not accessible by integration

주로 다음 상황에서 발생합니다.

  • PR 이벤트에서 GITHUB_TOKEN으로 쓰기 작업 수행(예: 코멘트 작성, 라벨 변경, 릴리스 생성)
  • 포크 PR에서 시크릿 접근 또는 쓰기 권한 작업 시도

이 메시지는 “액션이 설치된 통합(App)으로서 접근 불가”라는 의미로, 권한/이벤트 컨텍스트 문제일 확률이 큽니다.

에러 B: 403: Write access to repository not granted

토큰이 존재하지만 contents: write 같은 권한이 없을 때 흔합니다.

  • 태그 푸시
  • 릴리스 생성
  • gh CLI로 PR/릴리스/이슈 조작

에러 C: OIDC 관련 No OpenIDConnect provider found 또는 id-token permission is required

OIDC 연동을 시도했지만,

  • 워크플로 권한에 id-token: write가 없거나
  • 클라우드 측 OIDC Provider/Workload Identity 설정이 없거나
  • 조건(클레임 매칭)이 틀린 경우입니다.

3) 해결의 출발점: permissions를 명시하라

GitHub Actions는 워크플로/잡 레벨에서 권한을 명시할 수 있습니다. 최소 권한 원칙으로 시작하고, 필요한 권한만 추가하세요.

기본 예시: 읽기만 필요한 CI

name: ci
on:
  push:
    branches: [ main ]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

릴리스 생성/태그 푸시가 필요한 경우

릴리스 생성은 보통 contents: write가 필요합니다.

name: release
on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

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

여기서 중요한 점은 GITHUB_TOKENGH_TOKEN으로 넘겨 gh가 사용하게 만드는 것입니다.

PR 코멘트/이슈 조작이 필요한 경우

PR 코멘트는 pull-requests: write 또는 issues: write가 필요합니다(행동에 따라 다름).

permissions:
  contents: read
  pull-requests: write

jobs:
  comment:
    runs-on: ubuntu-latest
    steps:
      - name: Comment on PR
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh pr comment "${{ github.event.pull_request.number }}" \
            --body "CI 결과를 확인해주세요."

4) PR 이벤트(특히 포크 PR)에서 권한이 줄어드는 이유

PR은 공격 표면이 큽니다. 포크 PR에서는 악성 코드가 워크플로에서 시크릿을 탈취하거나, 리포지토리에 쓰기 작업을 시도할 수 있습니다. 그래서 GitHub는 기본적으로 다음을 제한합니다.

  • 포크 PR에서는 GITHUB_TOKEN 쓰기 권한이 강하게 제한
  • 시크릿 접근 불가(기본)

패턴 1: pull_request 대신 pull_request_target 사용(주의 필요)

pull_request_target은 베이스 브랜치 컨텍스트에서 실행되어 시크릿/권한이 더 열립니다. 하지만 PR에서 온 코드를 그대로 체크아웃해 실행하면 매우 위험합니다.

안전한 사용 원칙:

  • pull_request_target에서는 PR의 코드를 실행하지 말고, 메타 작업(라벨링, 코멘트 등)만 수행
  • 빌드/테스트는 pull_request에서 수행
name: pr-labeler
on:
  pull_request_target:
    types: [opened, synchronize]

permissions:
  pull-requests: write

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

패턴 2: 포크 PR에서 배포를 막고, 메인 브랜치에서만 배포

배포 잡은 push on main 또는 workflow_dispatch로만 돌리세요.

on:
  push:
    branches: [ main ]
  workflow_dispatch:

jobs:
  deploy:
    if: ${{ github.ref == 'refs/heads/main' }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "deploy"

5) OIDC로 클라우드 권한 에러 해결하기

장기 시크릿 기반 배포는 결국 만료/회전/유출 위험이 있습니다. OIDC는 이 문제를 구조적으로 줄입니다. 다만 설정이 하나라도 빠지면 권한 에러가 나기 쉬우므로 “워크플로 권한”과 “클라우드 신뢰 정책”을 같이 봐야 합니다.

5-1) 워크플로에서 id-token: write 추가

OIDC를 쓰려면 아래 권한이 필요합니다.

permissions:
  contents: read
  id-token: write

여기서 id-token: write를 넣지 않으면, 액션이 OIDC 토큰을 요청할 수 없어 실패합니다.

5-2) AWS 예시: GitHub OIDC로 AssumeRole

AWS는 aws-actions/configure-aws-credentials를 많이 씁니다.

name: deploy-aws
on:
  push:
    branches: [ main ]

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: ap-northeast-2

      - name: Verify identity
        run: |
          aws sts get-caller-identity

AWS 측에서 자주 틀리는 지점:

  • IAM OIDC Provider가 token.actions.githubusercontent.com으로 등록되어 있는지
  • Role trust policy에서 sub 조건이 실제 워크플로 컨텍스트와 일치하는지

예를 들어 sub는 보통 다음 형태입니다.

  • repo:ORG/REPO:ref:refs/heads/main
  • 또는 환경을 쓰면 repo:ORG/REPO:environment:prod

이 값이 조금만 달라도 AccessDenied가 납니다.

5-3) Azure 예시: Federated Credential 사용

Azure는 azure/login 액션으로 OIDC 로그인을 합니다.

name: deploy-azure
on:
  push:
    branches: [ main ]

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Azure login via OIDC
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Who am I
        run: |
          az account show

Azure에서 막히는 포인트:

  • Entra ID App Registration 또는 Managed Identity에 Federated Credential이 정확히 등록되어 있는지
  • subjectrepo:ORG/REPO:ref:refs/heads/main 형태로 정확히 매칭되는지
  • audience가 기본값(api://AzureADTokenExchange)과 맞는지

6) 디버깅 체크리스트(재현 가능한 방식)

권한 에러는 “감”으로 고치면 재발합니다. 아래 순서로 확인하면 대부분 빠르게 좁혀집니다.

6-1) 어떤 토큰을 쓰고 있는지 확인

  • secrets.GITHUB_TOKEN을 쓰는지
  • PAT를 쓰는지(가능하면 지양)
  • OIDC를 쓰는지

6-2) 워크플로 permissions가 잡/워크플로 레벨에 선언되어 있는지

특정 잡만 권한이 더 필요하면 잡 레벨로 더 좁게 주는 게 안전합니다.

permissions:
  contents: read

jobs:
  publish:
    permissions:
      contents: write
    runs-on: ubuntu-latest
    steps:
      - run: echo "publish"

6-3) 이벤트 컨텍스트 확인

  • pull_request인지 push인지
  • 포크 PR인지
  • pull_request_target인지

6-4) 클라우드라면 “GitHub 쪽 권한”과 “클라우드 쪽 정책”을 분리해서 본다

  • GitHub: id-token: write 누락 여부
  • AWS/Azure/GCP: trust policy 또는 federated credential의 sub/aud 매칭

이 접근은 OAuth 콜백이나 프록시 헤더처럼, 한쪽만 보고 고치면 계속 삽질하는 유형의 문제와 유사합니다. 네트워크/인증 경계에서 문제가 생기면 양쪽을 동시에 점검해야 합니다. 참고: Nginx OAuth 콜백 400 해결 - proxy_set_header·HTTPS

7) 보안 관점 권장안(실무 베스트 프랙티스)

  1. 기본값에 기대지 말고 permissions를 명시
  2. 가능하면 GITHUB_TOKEN으로 충분한 작업만 수행하고, 외부 클라우드는 OIDC로 전환
  3. 배포는 main 또는 승인된 환경(environment)에서만 실행
  4. 포크 PR에서는 시크릿/배포 작업을 원천 차단
  5. OIDC 신뢰 정책은 repo, ref, environment 단위로 최대한 좁게

특히 OIDC는 “편해서”가 아니라 “유출 면적을 줄이기 위해” 쓰는 방식입니다. 장기 키가 없어지면 운영 리스크가 크게 줄고, 권한 에러도 신뢰 정책과 워크플로 권한을 명시적으로 관리하면서 오히려 예측 가능해집니다.

8) 마무리: 에러 메시지를 권한 모델로 번역하라

GitHub Actions 권한 에러는 대부분 아래 두 문장으로 귀결됩니다.

  • GITHUB_TOKEN은 있다. 하지만 지금 이벤트/설정에서 쓰기 권한이 없다.
  • OIDC는 편하다. 하지만 id-token: write와 클라우드 신뢰 정책이 정확해야 한다.

다음에 403이나 Resource not accessible by integration을 만나면, 먼저 워크플로의 permissions와 이벤트 컨텍스트를 확인하고, 클라우드 연동이라면 OIDC 클레임 매칭까지 한 번에 점검하세요. 이렇게 하면 “한 번 고치고 끝”에 가까운 형태로 정리할 수 있습니다.