- Published on
GitHub Actions OIDC AssumeRole 실패 원인과 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스/CI 환경에서 정적 AWS 키를 없애기 위해 GitHub Actions의 OIDC(OpenID Connect)로 sts:AssumeRoleWithWebIdentity를 쓰는 구성이 사실상 표준이 됐습니다. 그런데 막상 적용하면 AssumeRole 단계에서 애매한 에러가 나고, AWS 콘솔/CloudTrail을 봐도 “권한 없음(AccessDenied)” 정도로만 보여서 삽질이 길어지기 쉽습니다.
이 글은 GitHub Actions OIDC로 AssumeRole이 실패할 때 가장 흔한 실패 지점을 로그/정책/토큰 관점으로 분해하고, 바로 적용 가능한 Trust Policy/Workflow 예제로 해결하는 방법을 정리합니다.
또한 CI 디버깅 관점은 GitHub Actions 캐시가 안 먹을 때 속도 3배 올린 실전처럼 “증상→원인→검증”의 흐름이 중요합니다. OIDC도 동일하게 증상(에러 메시지) 를 기준으로 원인 후보를 좁혀 가는 게 핵심입니다.
OIDC AssumeRole 흐름(정상 시나리오)
정상 흐름을 먼저 명확히 해두면, 어디서 끊겼는지 빠르게 찾을 수 있습니다.
- GitHub Actions 러너가
permissions: id-token: write권한을 통해 OIDC 토큰(JWT)을 발급받음 aws-actions/configure-aws-credentials가 그 토큰을 사용해 AWS STS에AssumeRoleWithWebIdentity호출- AWS IAM Role의 Trust Policy가 해당 OIDC Provider(issuer)와 claim 조건(
sub,aud)을 만족하면 임시 자격증명 발급 - 이후 AWS API 호출은 임시 자격증명으로 수행
실패는 보통 2~3번 경계에서 발생합니다.
대표 에러 메시지별 원인 맵
실제로 자주 보는 에러들을 “원인 후보”로 매핑해 두면, 디버깅 시간이 크게 줄어듭니다.
1) Not authorized to perform sts:AssumeRoleWithWebIdentity
- Trust Policy에
sts:AssumeRoleWithWebIdentity가 없거나 - OIDC Provider ARN/issuer가 잘못됐거나
Condition의sub/aud가 토큰과 불일치하거나- Role ARN을 잘못 지정했거나(다른 계정/다른 Role)
2) No OpenIDConnect provider found in your account for https://token.actions.githubusercontent.com
- IAM에 OIDC Provider를 생성하지 않았거나
- issuer URL이 다르거나(오타/슬래시) 다른 리전/계정에 만들어둠
3) The security token included in the request is invalid
id-token: write권한이 없어서 토큰이 발급되지 않았는데, 워크플로가 그 상태로 진행- 액션 버전/환경변수 꼬임으로 잘못된 토큰 사용
4) AccessDenied인데 Trust는 맞는 것 같음
- AssumeRole은 성공했지만, Role에 붙은 Permission Policy가 실제 AWS API 권한을 막음
- 또는 세션 정책/permission boundary/SCP(Organizations)로 차단
1단계: GitHub Actions 워크플로 권한부터 확인
OIDC는 GitHub 쪽에서 토큰을 발급받는 권한이 필요합니다. 아래가 빠지면 100% 실패합니다.
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: Configure AWS credentials (OIDC)
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy-role
aws-region: ap-northeast-2
- name: Who am I
run: aws sts get-caller-identity
여기서 permissions가 누락되면 configure-aws-credentials가 내부적으로 토큰을 못 받아오고, 결과적으로 STS 호출이 실패합니다.
디버깅 팁: 액션 로그 레벨 올리기
env:
ACTIONS_STEP_DEBUG: true
또는 리포지토리 Settings에서 ACTIONS_STEP_DEBUG 시크릿을 true로 설정해 단계 로그를 더 자세히 볼 수 있습니다.
2단계: AWS IAM OIDC Provider 생성 확인
AWS 계정에 GitHub OIDC Provider가 있어야 합니다.
- Issuer:
https://token.actions.githubusercontent.com - Audience: 보통
sts.amazonaws.com
CLI로 확인:
aws iam list-open-id-connect-providers
aws iam get-open-id-connect-provider \
--open-id-connect-provider-arn arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com
No OpenIDConnect provider found... 류의 에러면 이 단계가 1순위입니다.
3단계: Trust Policy(가장 흔한 실패 지점)
AssumeRole 실패의 70~80%는 Trust Policy의 Condition에서 발생합니다. 특히 sub 조건이 실제 워크플로의 컨텍스트와 조금이라도 다르면 바로 거절됩니다.
권장 Trust Policy 예시(최소 권한)
아래는 특정 리포지토리의 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",
"token.actions.githubusercontent.com:sub": "repo:ORG/REPO:ref:refs/heads/main"
}
}
}
]
}
여기서 자주 틀리는 포인트
ORG/REPO대소문자 불일치- 브랜치가
main이 아닌데main으로 고정 - 태그 배포인데
ref:refs/tags/v1.2.3형태를 고려하지 않음 - PR 이벤트인데
ref가 다르게 나오는 케이스를 고려하지 않음
태그/브랜치/환경별로 조건을 유연하게 만들기
브랜치/태그 모두 허용하려면 StringLike를 사용합니다.
{
"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/*",
"repo:ORG/REPO:ref:refs/tags/*"
]
}
}
}
운영에서는 보통 heads/*를 열기보다는, 배포 브랜치/태그만 명시적으로 좁히는 편이 안전합니다.
4단계: 실제 OIDC 토큰의 claim 확인(재현 가능한 진단)
Trust Policy의 sub/aud가 맞는지 확인하려면, “내가 생각하는 값”이 아니라 실제 발급된 JWT의 claim을 확인해야 합니다.
GitHub에서 OIDC 토큰을 직접 받아 디코딩하는 예시입니다(디버깅용). 토큰은 민감정보이므로 로그에 그대로 남기지 않도록 주의하세요.
- name: Mint OIDC token (debug)
id: oidc
run: |
echo "Requesting OIDC token..."
resp=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com")
echo "$resp" | jq -r '.value' > token.jwt
# JWT payload만 추출(로그 최소화)
python3 - <<'PY'
import json,base64
jwt=open('token.jwt').read().strip()
parts=jwt.split('.')
payload=parts[1] + '=='
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
PY
출력에서 아래를 확인합니다.
aud가sts.amazonaws.com인지sub가repo:ORG/REPO:ref:...형태로 무엇인지repository,ref,actor등이 기대한 값인지
이 결과와 Trust Policy의 조건을 1:1로 맞추면 대부분 해결됩니다.
5단계: AssumeRole은 성공했는데 AWS API가 실패하는 경우
aws sts get-caller-identity는 되는데, 예를 들어 S3 업로드나 ECR 푸시가 실패한다면, OIDC/Trust 문제가 아니라 Role Permission Policy 문제입니다.
예: ECR 푸시 권한이 빠진 경우
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
],
"Resource": "*"
}
]
}
또한 AWS Organizations의 SCP, IAM Permission Boundary, Session Policy가 끼어 있으면 “정책은 있어 보이는데 계속 AccessDenied”가 발생할 수 있습니다. 이때는 CloudTrail에서 거절 원인을 확인하는 게 가장 빠릅니다.
6단계: CloudTrail로 실패 원인 확정하기
AssumeRole 실패는 AWS 쪽에서 최종 판정이 나므로, CloudTrail 이벤트로 보면 결론이 빨라집니다.
- Event source:
sts.amazonaws.com - Event name:
AssumeRoleWithWebIdentity - Error code/message와 request parameters 확인
특히 sub mismatch는 CloudTrail의 request/response만으론 직접적으로 “sub가 달라서 실패”라고 친절히 안 나오는 경우가 많습니다. 그럴 땐 위에서 소개한 토큰 claim 디코딩과 병행해야 합니다.
실전 체크리스트(10분 컷)
- 워크플로에
permissions: id-token: write있는가 aws-actions/configure-aws-credentials@v4사용 중인가(구버전 이슈 방지)- AWS 계정에
token.actions.githubusercontent.comOIDC Provider가 존재하는가 - Trust Policy의
Principal.FederatedARN이 정확한가 - Trust Policy의
aud가sts.amazonaws.com으로 일치하는가 - Trust Policy의
sub조건이 실제 토큰의sub와 일치하는가(브랜치/태그/PR 이벤트 포함) - AssumeRole 성공 후 API 실패라면 Role Permission Policy/SCP/Boundary를 의심
CI 문제는 원인을 좁혀가는 과정이 중요합니다. 캐시 문제를 진단할 때도 단계별로 관찰 지점을 만드는 게 핵심이듯(GitHub Actions 캐시가 안 먹을 때 속도 3배 올린 실전), OIDC도 토큰 발급 → STS 호출 → Trust Policy 조건 매칭 → 권한 정책 순서로 나누면 거의 항상 답이 나옵니다.
마무리: 가장 안전한 운영 패턴
- Trust Policy의
sub는 가능한 한 리포지토리 + 배포 브랜치/태그로 좁히기 StringLike로 와일드카드를 쓰더라도 범위를 최소화- 워크플로 이벤트(푸시/태그/PR/환경 승인)에 따라
sub가 달라질 수 있음을 전제로 설계 - 디버깅 시에는 토큰 claim을 직접 확인해 “내가 믿는 값”을 “실제 값”으로 교체
이 패턴으로 구성하면 GitHub Actions OIDC AssumeRole 실패는 대부분 짧게 끝낼 수 있고, 정적 키 없이도 안전한 배포 파이프라인을 운영할 수 있습니다.