- Published on
GitHub Actions OIDC 401 권한 오류 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스/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는 크게 두 단계입니다.
- GitHub가 OIDC ID Token을 발급
- 워크플로가
id-token: write권한을 가지고 있어야 함 audience(aud) 값이 호출한 쪽과 신뢰 정책이 기대하는 값과 맞아야 함
- 클라우드 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를 신뢰하고, aud와 sub를 제한하는 전형적인 형태입니다.
{
"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_requestvspull_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가 기대하는 audiencerepository,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을 보면 항상 아래 순서로 분리하세요.
- OIDC 토큰을 받았는가? (워크플로
id-token: write) - STS Assume이 되는가? (Trust policy: provider/aud/sub)
- Assume 이후 필요한 AWS 권한이 있는가? (Role policy)
ECR 401, MissingAuthenticationToken처럼 “인증 토큰/자격증명 체인”이 깨질 때의 디버깅 감각은 아래 글도 도움이 됩니다.
- Kubernetes ImagePullBackOff 401 - ECR·IRSA·imagePullSecrets
- EKS에서 AWS SDK 403 MissingAuthenticationToken 해결
7) 최종 체크리스트(현장에서 바로 쓰는 버전)
배포 파이프라인에서 GitHub Actions OIDC 401이 나면 아래를 위에서부터 체크하세요.
- 워크플로에
permissions: id-token: write가 있는가? - Org/Repo 설정에서 Actions 권한이 제한되어 있지 않은가?
- fork PR에서 배포 잡을 돌리고 있지 않은가?
- OIDC 토큰의
sub,aud를 실제로 디코드해서 확인했는가? - IAM Trust policy의 Provider ARN이 정확한가?
- Trust policy의
aud조건이sts.amazonaws.com(또는 사용하는 값)과 일치하는가? - Trust policy의
sub조건이 이벤트(ref/branch/tag/environment)와 일치하는가? - 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을 “감”으로 고치지 않고 재현 가능한 방식으로 해결할 수 있습니다.