- Published on
GitHub Actions OIDC assume-role 실패 원인별 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
GitHub Actions에서 AWS 배포를 OIDC(OpenID Connect)로 전환하면 장기 액세스 키를 없앨 수 있어 보안과 운영이 크게 좋아집니다. 하지만 막상 aws-actions/configure-aws-credentials를 붙였는데 assume-role 단계에서 실패하면, 원인이 IAM Role 신뢰 정책(Trust Policy), OIDC 토큰 클레임 조건(sub/aud), 워크플로 권한(id-token), 조직/리포 정책, 세션 이름/조건 등으로 다양해 진단이 까다롭습니다.
이 글은 실패 로그를 기준으로 가장 자주 만나는 케이스를 “증상 → 원인 → 해결”로 정리합니다. OIDC 자체 개념/권한 설계는 아래 글도 함께 보면 전체 그림이 빨리 잡힙니다.
OIDC assume-role 흐름 한 장 요약
- GitHub Actions 러너가 OIDC 토큰(JWT)을 발급받음 (
id-token: write필요) sts:AssumeRoleWithWebIdentity호출- AWS IAM Role의 신뢰 정책이 토큰의
iss/aud/sub등 조건을 만족하면 임시 자격 증명 발급 - 이후 AWS API 호출은 Role에 붙은 권한 정책(Permission Policy) 로 통제
즉, 실패는 크게 두 갈래입니다.
- 신뢰 정책 불일치: STS가 Role을 “신뢰할 수 없음”으로 판단
- 권한 정책 부족: Role은 assume 성공했지만 이후 API에서 AccessDenied
이번 글은 “assume-role 실패”에 집중합니다.
먼저 로그에서 확인할 3가지
1) 에러 메시지의 키워드
Not authorized to perform sts:AssumeRoleWithWebIdentityNo OpenIDConnect provider found in your accountThe requested role is not assumableInvalidIdentityToken/audience is invalid
2) 어떤 Action을 쓰는지
대부분 아래 액션을 씁니다.
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
aws-region: ap-northeast-2
3) 워크플로에 permissions가 있는지
OIDC 토큰 발급은 기본적으로 막혀 있는 경우가 많습니다.
permissions:
id-token: write
contents: read
이게 없으면 “assume-role 이전” 단계에서 토큰 자체를 못 받아 실패합니다.
케이스 1) id-token 권한 누락
증상
Error: Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers- 또는
OIDC token관련 메시지
원인
워크플로 권한에 id-token: write가 없어서 GitHub가 OIDC JWT를 발급해주지 않습니다.
해결
워크플로 최상단(또는 job 단위)에 명시합니다.
name: deploy
on:
push:
branches: ["main"]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
aws-region: ap-northeast-2
추가로, 조직/리포 설정에서 “Workflow permissions”가 Read-only로 강제되어 있으면 job에서 올려도 제한될 수 있습니다(Organization 정책 확인).
케이스 2) OIDC Provider(iam oidc provider) 미생성 또는 ARN 불일치
증상
No OpenIDConnect provider found in your account for https://token.actions.githubusercontent.com
원인
AWS 계정에 GitHub OIDC Provider가 없거나, Role 신뢰 정책에서 참조한 Provider ARN이 실제와 다릅니다.
해결
1) OIDC Provider 존재 확인
aws iam list-open-id-connect-providers
출력에 token.actions.githubusercontent.com가 없으면 생성해야 합니다.
2) OIDC Provider 생성(예시)
AWS 콘솔(IAM → Identity providers)에서 생성하거나 CLI로 생성합니다.
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
> Thumbprint는 문서/환경에 따라 변경될 수 있으니 AWS 공식 가이드를 기준으로 확인하세요.
3) Role 신뢰 정책의 Principal이 정확한지 확인
신뢰 정책에서 Federated가 정확한 Provider ARN을 가리켜야 합니다.
케이스 3) Trust Policy 조건의 sub가 실제와 불일치
증상
Not authorized to perform sts:AssumeRoleWithWebIdentity- 액션은 정상인데 STS에서 거절
원인
가장 흔한 실수입니다. sub(subject) 조건을 너무 타이트하게 걸었거나, 브랜치/환경/리포 이름이 달라 토큰의 sub와 매칭이 안 됩니다.
GitHub OIDC의 sub는 보통 아래 형태 중 하나입니다.
repo:ORG/REPO:ref:refs/heads/mainrepo:ORG/REPO:pull_requestrepo:ORG/REPO:environment:prod
해결
1) 우선 “정확한 sub”를 확인
가장 확실한 방법은 토큰 클레임을 출력하는 것입니다. configure-aws-credentials는 내부적으로 토큰을 사용하지만, 디버깅 단계에서 ACTIONS_ID_TOKEN_REQUEST_URL로 직접 받아 decode할 수 있습니다.
- name: Dump OIDC token claims (debug)
shell: bash
run: |
set -euo pipefail
echo "Requesting OIDC token..."
TOKEN_JSON=$(curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com")
TOKEN=$(python -c "import json,sys; print(json.load(sys.stdin)['value'])" <<< "$TOKEN_JSON")
# JWT payload decode (no signature verification; for debugging only)
python - <<'PY'
import os,base64,json
jwt=os.environ['TOKEN']
parts=jwt.split('.')
payload=parts[1] + '===' # pad
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
PY
env:
TOKEN: ${{ steps.get_token.outputs.token }}
위 예시는 그대로 동작하지 않을 수 있어(steps 출력 등) 실무에선 더 단순한 방식으로 구성합니다. 핵심은 JWT payload에서 sub, aud, iss를 확인하는 것입니다.
2) Trust Policy를 실제 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:YOURORG/YOURREPO:ref:refs/heads/main"
}
}
}
]
}
브랜치 패턴을 허용하려면:
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:YOURORG/YOURREPO:ref:refs/heads/*"
}
환경 보호(Environments)를 쓰면 environment:prod 형태로 바뀌므로 그에 맞춰야 합니다.
케이스 4) aud(audience) 불일치
증상
InvalidIdentityToken: audience is invalid- 또는 assume-role 거절
원인
Trust Policy에서 token.actions.githubusercontent.com:aud를 sts.amazonaws.com로 강제했는데, 실제 토큰 aud가 다르게 발급되었습니다. 흔한 원인은 토큰 발급 시 audience 파라미터를 다른 값으로 준 경우입니다.
해결
- 가장 표준은
aud = sts.amazonaws.com - Trust Policy도 이에 맞추고, 액션도 기본값을 따릅니다.
만약 의도적으로 커스텀 audience를 쓴다면 Trust Policy의 aud도 동일하게 맞추세요.
케이스 5) Role ARN/계정/이름 오타 또는 Path 포함
증상
The requested role is not assumable유사 메시지AccessDenied와 함께 role 찾기 실패처럼 보임
원인
role-to-assume에 적은 ARN이 실제 Role과 다릅니다. 특히 Role path(/service/gha-deploy)가 있는 경우 콘솔에서 보이는 이름만 복사해 붙이면 실패할 수 있습니다.
해결
CLI로 Role ARN을 정확히 확인합니다.
aws iam get-role --role-name gha-deploy
Path가 있다면 --role-name은 이름만이지만, 반환되는 Arn을 워크플로에 그대로 사용하세요.
케이스 6) GitHub 리포/브랜치/포크 이벤트 차이로 sub가 달라짐
증상
- main 브랜치 push는 되는데 PR에서만 실패
- 내부 PR은 되는데 fork PR에서만 실패
원인
이벤트 타입에 따라 sub가 달라집니다.
- PR:
repo:ORG/REPO:pull_request - fork PR: 보안상 OIDC 토큰 발급/권한이 제한될 수 있음(특히
pull_requestvspull_request_target차이)
해결
- 배포는 원칙적으로
push(protected branch)나workflow_dispatch로 제한 - PR에서 AWS 접근이 필요하면 최소권한 + 별도 Role + 조건을 분리
예: PR에서는 read-only Role만 허용하고, main 배포 Role은 ref:refs/heads/main만 허용.
케이스 7) Trust Policy는 맞는데, 세션 조건/태그 조건 때문에 거절
증상
Not authorized to perform sts:AssumeRoleWithWebIdentity- 또는
AccessDenied인데 Trust Policy가 맞아 보임
원인
Role 신뢰 정책에 sts:RoleSessionName, sts:TagSession, aws:RequestTag/* 같은 조건을 걸어둔 경우, 액션이 보내는 값과 불일치하면 assume 자체가 거절됩니다.
해결
- 우선 신뢰 정책을 단순화하여 assume 성공을 확인
- 이후 필요한 조건을 단계적으로 추가
configure-aws-credentials는 role-session-name을 지정할 수 있습니다.
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
role-session-name: gha-${{ github.run_id }}
aws-region: ap-northeast-2
빠른 체크리스트(10분 진단)
- 워크플로에
permissions: id-token: write있는가? - AWS 계정에 OIDC Provider가 존재하는가?
- Role Trust Policy의
Principal.Federated가 올바른 Provider ARN인가? - Trust Policy의
aud가sts.amazonaws.com과 일치하는가? - Trust Policy의
sub가 실제 이벤트(push/PR/env)와 일치하는가? - 리포 이름 대소문자/ORG 이름이 정확한가?
role-to-assumeARN이 정확한가?(path 포함 여부)- 조직 정책에서 OIDC/권한이 제한되어 있지 않은가?
- PR/fork 이벤트에서 토큰 발급 제한을 밟고 있지 않은가?
- 추가 조건(세션명/태그)이 있다면 일단 제거하고 최소 구성으로 성공시키는가?
권장하는 “안전한” Trust Policy 예시 2종
1) main 브랜치 배포 전용(가장 흔함)
{
"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:YOURORG/YOURREPO:ref:refs/heads/main"
}
}
}
]
}
2) Environment(prod) 승인 기반 배포
{
"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",
"token.actions.githubusercontent.com:sub": "repo:YOURORG/YOURREPO:environment:prod"
}
}
}
]
}
Environment를 쓰면 GitHub의 보호 규칙(승인, 대기 등)과 함께 배포 권한을 더 안전하게 묶을 수 있습니다.
마무리: “신뢰 정책”과 “토큰 클레임”을 맞추는 게임
OIDC assume-role 실패는 대부분 AWS가 받은 토큰 클레임과 IAM Role 신뢰 정책 조건이 1:1로 맞지 않아서 발생합니다. 따라서 해결의 핵심은 (1) 워크플로에서 토큰 발급이 가능한지 확인하고, (2) aud/sub를 실제 값 기준으로 맞추며, (3) 이벤트 타입(PR/branch/environment)에 따라 Role을 분리하는 것입니다.
배포 이후 단계에서 AccessDenied가 이어진다면, 그건 assume-role이 아니라 Role 권한 정책 문제일 가능성이 큽니다. OIDC 전반의 권한 설계/오류 패턴은 아래 글에서 더 확장된 케이스로 다뤘습니다.