Published on

GitHub Actions OIDC 401 권한 오류 해결 가이드

Authors

서버리스/CI 파이프라인에서 장기 키(Access Key, Secret)를 없애기 위해 GitHub Actions OIDC(OpenID Connect)를 붙이는 순간, 가장 흔하게 마주치는 게 401 Unauthorized 계열의 권한 오류입니다. 문제는 401이 “토큰이 없거나 잘못됐다”는 뜻으로 뭉뚱그려져 있어, 실제 원인(워크플로 권한/토큰 발급/신뢰 정책/클레임 조건/오디언스 불일치)이 어디인지 빠르게 좁히지 못하면 삽질이 길어진다는 점입니다.

이 글에서는 GitHub Actions OIDC 흐름을 짧게 정리한 뒤, 401이 나는 대표 케이스를 진단 체크리스트 → 로그로 확인 → 설정 수정 순서로 해결합니다. 예시는 AWS(AssumeRoleWithWebIdentity) 중심으로 들지만, Azure/GCP도 동일하게 “OIDC 토큰 발급 + IdP 신뢰 + 클레임 매칭”이 핵심입니다.

관련해서 EKS에서 인증/권한 오류를 다룬 글도 함께 보면 원인 분리가 빨라집니다: EKS에서 AWS SDK 403 MissingAuthenticationToken 해결, Kubernetes ImagePullBackOff 401 - ECR·IRSA·imagePullSecrets

OIDC 401이 터지는 지점: “토큰 발급” vs “토큰 교환(Assume)”

GitHub Actions OIDC는 크게 두 단계입니다.

  1. GitHub가 OIDC ID Token을 발급
  • 워크플로가 id-token: write 권한을 가지고 있어야 함
  • audience(aud) 값이 호출한 쪽과 신뢰 정책이 기대하는 값과 맞아야 함
  1. 클라우드 STS가 토큰을 받아 임시 자격증명으로 교환
  • AWS: AssumeRoleWithWebIdentity
  • Azure: Federated credential로 토큰 교환
  • GCP: Workload Identity Federation

401이 발생할 때는 로그에서 “어느 단계에서 401이 났는지”를 먼저 구분하세요.

  • 단계 1 실패: ACTIONS_ID_TOKEN_REQUEST_URL 관련 호출에서 401/403
  • 단계 2 실패: STS/클라우드 API가 401/403 (예: Not authorized to perform sts:AssumeRoleWithWebIdentity, InvalidIdentityToken, Audience mismatch 등)

1) 가장 흔한 원인: workflow permissions에 id-token이 없음

GitHub Actions에서 OIDC 토큰을 받으려면 워크플로에 다음 권한이 필요합니다.

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

permissions:
  id-token: write   # OIDC 토큰 발급에 필수
  contents: read    # 보통 checkout 때문에 필요

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Debug OIDC env
        run: |
          echo "ACTIONS_ID_TOKEN_REQUEST_URL=$ACTIONS_ID_TOKEN_REQUEST_URL"
          echo "ACTIONS_ID_TOKEN_REQUEST_TOKEN is set? ${ACTIONS_ID_TOKEN_REQUEST_TOKEN:+yes}"

진단 포인트

  • 레포 설정(Organization 정책 포함)에서 Workflow permissions가 Read-only로 강제되어 있거나, id-token이 기본적으로 막혀 있을 수 있습니다.
  • 특히 재사용 워크플로(workflow_call)나 환경 보호 규칙(environment protection)과 결합되면 권한이 제한된 상태로 실행되는 경우가 있습니다.

증상

  • actions/checkout은 되는데, aws-actions/configure-aws-credentials에서 401/403
  • 또는 토큰 요청 단계에서 401 Unauthorized

2) aws-actions/configure-aws-credentials 사용 시 401/403: role-to-assume와 OIDC 설정 불일치

AWS를 예로 들면, GitHub OIDC 토큰을 STS에 넘겨 AssumeRoleWithWebIdentity를 수행합니다. 여기서 401/403이 나면 대부분 아래 중 하나입니다.

  • IAM Role의 Trust policy에서 Provider/조건이 잘못됨
  • aud 조건이 불일치
  • sub 조건이 불일치(브랜치/태그/환경/PR 이벤트에 따라 sub가 달라짐)
  • OIDC Provider가 잘못 생성되었거나 thumbprint/URL이 틀림

정상 예시: 워크플로 구성

permissions:
  id-token: write
  contents: read

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/gh-actions-deploy
          aws-region: ap-northeast-2
          role-session-name: gh-actions-${{ github.run_id }}

      - name: WhoAmI
        run: aws sts get-caller-identity

Trust policy(중요) 예시

아래는 GitHub OIDC Provider를 신뢰하고, audsub를 제한하는 전형적인 형태입니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:ORG/REPO:ref:refs/heads/main"
        }
      }
    }
  ]
}

401/403을 만드는 흔한 실수

(1) sub가 실제 실행 컨텍스트와 다름

  • main 브랜치 push만 허용했는데, 실제로는 tag 릴리스(refs/tags/v1.2.3)에서 실행
  • PR에서 실행되는데 pull_request 이벤트의 sub 형태를 고려하지 않음
  • GitHub Environment를 사용하는데 sub가 environment: 형태로 나오는 케이스

해결은 “허용 범위를 넓히되, 최소 권한으로”입니다. 예:

"StringLike": {
  "token.actions.githubusercontent.com:sub": [
    "repo:ORG/REPO:ref:refs/heads/main",
    "repo:ORG/REPO:ref:refs/tags/*"
  ]
}

(2) aud 조건 불일치

aws-actions/configure-aws-credentials는 기본적으로 aud=sts.amazonaws.com을 사용합니다. Trust policy에 다른 aud를 넣었거나, 반대로 워크플로에서 aud를 커스텀했는데 Trust policy가 기본값만 허용하면 실패합니다.

  • Trust policy: aud=sts.amazonaws.com
  • 워크플로에서 audience: something-else를 사용 → InvalidIdentityToken/401/403

3) “토큰 발급 자체”가 401: fork PR / 보안 정책 / 권한 제한

GitHub는 보안상 fork에서 온 PR에 대해 토큰/시크릿 권한을 제한합니다. 특히 OIDC도 조직 정책/레포 설정에 따라 제한될 수 있습니다.

체크리스트

  • PR이 fork에서 왔는가?
  • pull_request vs pull_request_target 이벤트 차이 이해했는가?
    • pull_request_target은 베이스 레포 컨텍스트로 실행되어 권한이 강하지만, 체크아웃/실행 방식이 잘못되면 공급망 리스크가 커집니다.
  • 조직(Org) 단위에서 “GitHub Actions OIDC” 사용이 제한되어 있지 않은가?

권장 패턴(보안 고려)

  • 배포/권한이 필요한 잡은 push(보호된 브랜치)나 workflow_dispatch에서만 실행
  • PR에서는 빌드/테스트만 수행하고, 배포는 금지

4) 실제 OIDC 토큰 클레임을 눈으로 확인하는 디버깅

401의 핵심은 “내가 생각한 sub/aud가 실제 토큰과 같은가?”입니다. 토큰을 직접 디코드해서 확인하면 원인 파악이 빨라집니다.

아래는 GitHub가 제공하는 엔드포인트로 ID Token을 받아와서 payload를 출력하는 예시입니다.

permissions:
  id-token: write
  contents: read

jobs:
  debug-oidc:
    runs-on: ubuntu-latest
    steps:
      - name: Fetch OIDC token and print claims
        shell: bash
        run: |
          set -euo pipefail

          # OIDC 토큰 요청 (audience를 명시할 수도 있음)
          TOKEN_JSON=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com")

          ID_TOKEN=$(python - <<'PY'
import json,sys
print(json.load(sys.stdin)['value'])
PY
          <<< "$TOKEN_JSON")

          # JWT payload 디코드 (서명 검증은 생략: 디버깅 목적)
          python - <<'PY'
import os, json, base64

t = os.environ['ID_TOKEN']
payload = t.split('.')[1]
payload += '=' * (-len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
PY
        env:
          ID_TOKEN: ${{ steps.fetch.outputs.id_token }}

위 예시는 step output을 쓰지 않아서 그대로는 동작이 어색할 수 있습니다. 더 단순하게는 한 스텝 안에서 ID_TOKEN을 변수로 들고 바로 디코드하세요:

- name: Print OIDC claims (simple)
  shell: bash
  run: |
    set -euo pipefail
    TOKEN_JSON=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
      "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com")
    ID_TOKEN=$(python -c 'import json,sys; print(json.load(sys.stdin)["value"])' <<< "$TOKEN_JSON")

    python - <<'PY'
import json, base64, os
payload = os.environ['ID_TOKEN'].split('.')[1]
payload += '=' * (-len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
PY
  env:
    ID_TOKEN: ${{ env.ID_TOKEN }}

이때 확인해야 할 대표 클레임:

  • sub: repo/브랜치/태그/환경 정보
  • aud: STS가 기대하는 audience
  • repository, ref, actor, workflow

이 값을 기반으로 IAM Trust policy의 조건을 정확히 맞추면 401/403의 80%는 해결됩니다.

5) AWS OIDC Provider 자체 점검(issuer/URL/Thumbprint)

AWS IAM에 OIDC Provider를 만들 때 URL은 보통 아래입니다.

  • Issuer: https://token.actions.githubusercontent.com
  • Provider ARN: arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com

여기서 provider URL을 잘못 넣거나(https 포함 여부, 경로 포함 등), 조직 정책으로 인해 다른 issuer를 쓰는 엔터프라이즈 설정을 섞어버리면 Assume 단계에서 실패합니다.

확인 명령(예)

aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com

결과에서 Url, ClientIDList(aud 허용 목록), ThumbprintList를 확인하세요.

6) 자주 헷갈리는 포인트: 401 vs 403, 그리고 “권한”의 범위

  • OIDC 토큰 발급 실패는 GitHub 측 권한/정책 문제일 가능성이 큼
  • AssumeRoleWithWebIdentity 실패는 IAM 신뢰 정책/조건 불일치인 경우가 많음
  • Assume은 성공했는데 이후 AWS API 호출이 실패하면, 그건 Role permission policy(예: S3, ECR, CloudFormation 권한) 문제입니다.

즉, 401/403을 보면 항상 아래 순서로 분리하세요.

  1. OIDC 토큰을 받았는가? (워크플로 id-token: write)
  2. STS Assume이 되는가? (Trust policy: provider/aud/sub)
  3. Assume 이후 필요한 AWS 권한이 있는가? (Role policy)

ECR 401, MissingAuthenticationToken처럼 “인증 토큰/자격증명 체인”이 깨질 때의 디버깅 감각은 아래 글도 도움이 됩니다.

7) 최종 체크리스트(현장에서 바로 쓰는 버전)

배포 파이프라인에서 GitHub Actions OIDC 401이 나면 아래를 위에서부터 체크하세요.

  1. 워크플로에 permissions: id-token: write가 있는가?
  2. Org/Repo 설정에서 Actions 권한이 제한되어 있지 않은가?
  3. fork PR에서 배포 잡을 돌리고 있지 않은가?
  4. OIDC 토큰의 sub, aud를 실제로 디코드해서 확인했는가?
  5. IAM Trust policy의 Provider ARN이 정확한가?
  6. Trust policy의 aud 조건이 sts.amazonaws.com(또는 사용하는 값)과 일치하는가?
  7. Trust policy의 sub 조건이 이벤트(ref/branch/tag/environment)와 일치하는가?
  8. Assume 성공 후 AWS API 권한(Policy)이 충분한가?

결론

GitHub Actions OIDC의 401 Unauthorized는 대개 “토큰이 이상한가?”가 아니라 클레임(aud/sub)과 신뢰 정책이 미세하게 어긋난 문제입니다. 가장 빠른 해결법은 (1) id-token: write 권한을 명확히 주고, (2) OIDC 토큰 payload를 직접 디코드해 실제 sub/aud를 확인한 다음, (3) IAM Trust policy 조건을 그 값에 맞게 최소 권한으로 조정하는 것입니다.

위 과정을 한 번 템플릿으로 만들어두면, 레포/브랜치/환경이 늘어나도 401을 “감”으로 고치지 않고 재현 가능한 방식으로 해결할 수 있습니다.